Merge branch '!master' into ignite-11402

# Conflicts:
#	modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
diff --git a/.travis.yml b/.travis.yml
index d073814..837f7c4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,7 +21,10 @@
   include:
     - language: java
       os: linux
-      dist: xenial
+      dist: bionic
+      before_install:
+        - sudo apt-get update
+        - sudo apt-get -y install libnuma-dev
       install: skip
       jdk: openjdk8
       before_script:
@@ -32,7 +35,10 @@
 
     - language: java
       os: linux
-      dist: xenial
+      dist: bionic
+      before_install:
+        - sudo apt-get update
+        - sudo apt-get -y install libnuma-dev
       install: skip
       jdk: openjdk11
       before_script:
@@ -58,7 +64,10 @@
     - language: java
       name: "Check test suites"
       os: linux
-      dist: xenial
+      dist: bionic
+      before_install:
+        - sudo apt-get update
+        - sudo apt-get -y install libnuma-dev
       install: skip
       jdk: openjdk8
       script: mvn test -Pcheck-test-suites,all-java,all-scala,scala -B -V
diff --git a/README.md b/README.md
index 1dc9879..cc892f9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Apache Ignite
 
-<a href="https://ignite.apache.org/"><img src="https://github.com/apache/ignite-website/blob/master/images/ignite_logo_full.svg" hspace="20"/></a>
+<a href="https://ignite.apache.org/"><img src="https://github.com/apache/ignite-website/blob/master/assets/images/apache_ignite_logo.svg" hspace="20"/></a>
 
 [![Build Status](https://travis-ci.org/apache/ignite.svg?branch=master)](https://travis-ci.org/apache/ignite)
 [![GitHub](https://img.shields.io/github/license/apache/ignite?color=blue)](https://www.apache.org/licenses/LICENSE-2.0.html)
@@ -15,7 +15,7 @@
 
 <p align="center">
     <a href="https://ignite.apache.org">
-        <img src="https://github.com/apache/ignite-website/blob/master/images/png-diagrams/ignite_cluster.png" width="400px"/>
+        <img src="https://github.com/apache/ignite-website/blob/master/docs/2.9.0/images/ignite_clustering.png" width="400px"/>
     </a>
 </p>
 
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 8e56de2..bbed104 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,6 +1,152 @@
 Apache Ignite Release Notes
 ===========================
 
+Apache Ignite In-Memory Distributed Database 2.12.0
+-----------------------------------------------------------
+
+(!) WARNINGS:
+* The community accepted deprecate for removal in future releases: CacheMode#LOCAL, CacheAtomicityMode#TRANSACTIONAL_SNAPSHOT, CacheConfiguration#rebalanceDelay.
+* GCE, AWS, Azure modules, CacheSpringStoreSessionListener, and TcpDiscoveryZookeeperIpFinder migrated to the Ignite extensions.
+* The deprecated legacy service grid implementation will be removed in the next release.
+
+Ignite:
+* Added Ignite Distributed Environment Tests.
+* Added IndexQuery API for fast index scans.
+* Added KubernetesConnectionConfiguration.discoveryPort.
+* Added MergeSort distributed cache query reducer.
+* Added Read Repair on specified partition to the Control.sh.
+* Added an ability to track request handling completion in GridRestProcessor.
+* Added an explicit method to register binary type based on class.
+* Added batch cache operations histogram metrics.
+* Added benchmarks for cache queries - scan, index, text.
+* Added docker image for s390x.
+* Added events for snapshot restore operation.
+* Added expire policy info into log of started cache.
+* Added firing event for any ReadRepair attempt (if consistency violation found). Atomic caches are also supported.
+* Added offline utility to read and analyze index files.
+* Added possibility to accept while indexing classes/enums that are extending interfaces/classes marked to be stored in cache.
+* Added rename index tree operation with corresponding WAL record.
+* Added resource de-allocation in SharedPageLockTracker.
+* Added snapshot thread pool configuration.
+* Added support for creating IndexQuery without index name.
+* Added the ability to cancellation of consistency recovery command (Read Repair via control.ch).
+* Added the ability to record Control.sh consistency check violations to other log file.
+* Added the ability to restore snapshot taken on different topologies.
+* Added the ability to snapshot encrypted caches.
+* Added the cache destroy command for control.sh.
+* Added the force deactivation flag to the IgniteMXBean.
+* Added time metrics and statistics for the IgniteCache#getAllOutTx.
+* Added unconditional logging of tx states to WAL to ensure correct tx recovery after node crash.
+* Changed IGNITE_PDS_WAL_REBALANCE_THRESHOLD from System property to Distributed property.
+* Deprecated CacheConfiguration#rebalanceDelay for removal.
+* Deprecated CacheMode#LOCAL for removal.
+* Deprecated CacheAtomicityMode#TRANSACTIONAL_SNAPSHOT deprecated for removal.
+* Deprecated the IGNITE_THRESHOLD_WAL_ARCHIVE_SIZE_PERCENTAGE system property.
+* Expanded kubernetes examples to include full beans.
+* Fixed AssertionError: Unexpected rebalance on rebalanced cluster.
+* Fixed CacheObjectAdapter#put incorrect offset handling.
+* Fixed NPE on remote listener registration with null remote filter and security enabled.
+* Fixed PagesWriteSpeedBasedThrottle time to throttle calculation.
+* Fixed REST and Zookeeper module logging: use the slf4j facade to log third-party libraries.
+* Fixed REST request failure when cache node filter is used.
+* Fixed SSL read error.
+* Fixed StackOverflowError in case if exception suppressed with itself.
+* Fixed adaptation of the historical rebalance to the release of WAL segments.
+* Fixed an error when starting a node due to exceeding the DataStorageConfiguration#getMaxWalArchiveSize.
+* Fixed azure-blob-storage dependency versions.
+* Fixed builds with maven 3.8.1+.
+* Fixed cancelling WAL segments reservation when max WAL archive size is reached.
+* Fixed change permissions required to create/destroy caches in GridRestProcessor.
+* Fixed check of SERVICE_DEPLOY permission.
+* Fixed check statistics obsolescence on server nodes only.
+* Fixed client node reconnect with enabled security.
+* Fixed concurrent heartbeat update while in blocking section for system workers.
+* Fixed contention in lock on Compound future.
+* Fixed diagnostic information for PDS corruption scenarios.
+* Fixed error extension about B+tree lock retry for indexes.
+* Fixed exception for checkpoint marker reading error.
+* Fixed exception message of closed the GridCloseableIteratorAdapter.
+* Fixed execution of daemon node operations that require authorization.
+* Fixed fallback to full rebalance in case of historical rebalancing failure.
+* Fixed handle windows in ODBC on Windows.
+* Fixed idle verify and snapshot check ambiguity error output.
+* Fixed multiple results bug when query parallelism is enabled for single partition query.
+* Fixed node restart in maintenance mode with security enabled.
+* Fixed performance suggestion URL to legacy documentation.
+* Fixed preconfigured service deployment authorization.
+* Fixed security context propagation for cache event.
+* Fixed security context propagation for compute tasks.
+* Fixed snapshot restore fails if metadata is missing on any baseline node.
+* Fixed snapshot restore on not all affinity partitions are physically present.
+* Fixed spontaneous SocketTimeoutException in server socket accept (JDK-8247750).
+* Fixed storage of physical pageIds in a DurableBackgroundCleanupIndexTreeTask.
+* Fixed the AssertionError when the JmxMetricExporterSpi unregister a filtered metric registry.
+* Fixed the data structures system views registration on inactive cluster start.
+* Fixed the inconsistency of the built new indexes after restarting the node.
+* Fixed the remove metric value is different for sync and async methods.
+* Fixed triple flushing of meta information at the checkpoint.
+* Fixed unconditional Lucene index creation.
+* Fixed unnecessary socket shutdown and close log output.
+* Fixed walTotalSize incorrectly reported when wal archiving is turned off.
+* Implemented CDC metrics.
+* Implemented Change Data Capture.
+* Implemented IndexQuery filter operation.
+* Implemented Yardstick benchmark for multi cache transaction operations.
+* Implemented forbid duplicated field in CREATE INDEX clause.
+* Improved logging of the peer class loading error message.
+* Improved the snapshot procedure logging.
+* Migrated CacheSpringStoreSessionListener to the Ignite extensions.
+* Migrated TcpDiscoveryZookeeperIpFinder to the Ignite extensions.
+* Migrated gce, aws, azure modules to the Ignite extensions.
+* Updated Bouncycastle version (fixes CVE-2020-15522, CVE-2020-0187, CVE-2020-26939).
+* Updated log4j version to 2.17.1 (fixes CVE-2021-44228, CVE-2021-44832, CVE-2021-45046, CVE-2021-45105).
+* Updated PostgreSQL JDBC Driver version (fixes CVE-2020-13692).
+* Updated httpclient, httpcore versions (fixes CVE-2020-13956).
+* Updated the Jackson dependency version (fixes CVE-2019-16942, CVE-2019-16943, CVE-2019-17531).
+* Updated the MySql connector dependency version (fixes CVE-2019-2692).
+* Updated the Netty dependency version (fixes CVE-2021-21295).
+
+Java thin-client:
+* Added SQLSTATE to thin client SQL error message.
+* Added client cache for OptimizedMarshaller class names.
+* Added partition awareness for ScanQuery with specified partition.
+* Added requests thread pool monitoring.
+* Fixed ClassNotFoundException on service call after failover.
+* Fixed follow user-defined endpoint order, try default port first.
+* Fixed issue with explicit binary type configuration.
+* Fixed transaction failure after timeout.
+
+.Net:
+* Fixed NRE in ClientFailoverSocket due to late logger setter.
+* Fixed NullPointerException in ContinuousQuery with security enabled.
+* Fixed Schema project version and examples packaging.
+* Fixed SslStreamFactory.CertificatePath null value.
+* Fixed TypeNameParser to ignore escaped characters in compiler-generated type names.
+* Fixed dynamic assemblies handling in TypeResolver.
+* Fixed thin client streamer not creating SQL table entries.
+* Fixed verify-nuget.ps1 failure when .NET 5 is installed.
+
+Ignite C++:
+* Added Compute task functionality.
+* Added support for affinity fields.
+* Extended platforms API to call Java versioned entry processor.
+* Fixed compilation on Visual Studio.
+* Implemented building windows installer of ODBC Driver on CMake.
+* Removed separate JNI module and moved it to Core.
+
+SQL:
+* Added support of precision parameter for varbinary type.
+* Fixed PK flag ordering in SQL AST traverse.
+* Fixed incorrect JOIN when querying a single-node cluster.
+* Fixed setting alias for affinity fields.
+* Implemented table statistics.
+
+Apache Ignite In-Memory Distributed Database 2.11.1
+-----------------------------------------------------------
+
+* Fixed CVE-2021-44228, CVE-2021-45046, CVE-2021-45105 of ignite-log4j2 module by updating dependency log4j up to 2.17.0.
+* Added ability to build ODBC installers using CMake
+
 Apache Ignite In-Memory Distributed Database 2.11.0
 -----------------------------------------------------------
 
diff --git a/bin/ignite-cdc.sh b/bin/ignite-cdc.sh
index b04345d..13790fa 100755
--- a/bin/ignite-cdc.sh
+++ b/bin/ignite-cdc.sh
@@ -24,4 +24,8 @@
     else IGNITE_HOME_TMP=${IGNITE_HOME};
 fi
 
+if ! [ "${CDC_JVM_OPTS:-}" = "" ]; then
+    export JVM_OPTS=$CDC_JVM_OPTS
+fi
+
 ${IGNITE_HOME_TMP}/bin/ignite.sh "$@"
diff --git a/docs/_data/toc.yaml b/docs/_data/toc.yaml
index ac3afce..29b14ce 100644
--- a/docs/_data/toc.yaml
+++ b/docs/_data/toc.yaml
@@ -147,6 +147,8 @@
       url: persistence/disk-compression
     - title: Tuning Persistence
       url: persistence/persistence-tuning
+    - title: Change Data Capture
+      url: persistence/change-data-capture
 - title: Cluster Snapshots
   url: snapshots/snapshots
 - title: Configuring Caches
diff --git a/docs/_docs/SQL/ODBC/odbc-driver.adoc b/docs/_docs/SQL/ODBC/odbc-driver.adoc
index 9f4e9b8..b707b09 100644
--- a/docs/_docs/SQL/ODBC/odbc-driver.adoc
+++ b/docs/_docs/SQL/ODBC/odbc-driver.adoc
@@ -163,62 +163,57 @@
 
 == Building ODBC Driver
 
-Ignite is shipped with pre-built installers for both 32- and 64-bit versions of the driver for Windows. So if you just want to install ODBC driver on Windows you may go straight to the <<Installing ODBC Driver>> section for installation instructions.
+Ignite is shipped with pre-built installers for both 32- and 64-bit versions of the driver for Windows.
+So if you just want to install ODBC driver on Windows, you may go straight to the <<Installing ODBC Driver>>
+section for installation instructions.
 
-If you use Linux you will still need to build ODBC driver before you can install it. So if you are using Linux or if you still want to build the driver by yourself for Windows, then keep reading.
-
-Ignite ODBC Driver source code is shipped as part of the Ignite package and it should be built before usage.
-
-Since the ODBC Driver is written in {cpp}, it is shipped as part of Ignite {cpp} and depends on some of the {cpp} libraries. More specifically, it depends on the `utils` and `binary` Ignite libraries. This means that you will need to build them prior to building the ODBC driver itself.
-
-We assume here that you are using the binary Ignite release. If you are using the source release, instead of `%IGNITE_HOME%\platforms\cpp` path you should use `%IGNITE_HOME%\modules\platforms\cpp` throughout.
+For Linux, you will still need to build an ODBC driver before installing it.
+So if you are using Linux or still want to build the driver yourself for Windows, refer to the next section.
 
 === Building on Windows
 
-You will need MS Visual Studio 2010 or later to be able to build the ODBC driver on Windows. Once you have it, open Ignite solution `%IGNITE_HOME%\platforms\cpp\project\vs\ignite.sln` (or `ignite_86.sln` if you are running 32-bit platform), left-click on odbc project in the "Solution Explorer" and choose "Build". Visual Studio will automatically detect and build all the necessary dependencies.
+To start with, install the dependencies below:
 
-The path to the .sln file may vary depending on whether you're building from source files or binaries. If you don't see your .sln file in `%IGNITE_HOME%\platforms\cpp\project\vs\`, try looking in `%IGNITE_HOME%\modules\platforms\cpp\project\vs\`.
+* MS Visual C++ (10.0 and up), g++ (4.4.0 and up)
+* OpenSSL (32-bit or 64-bit versions)
+* CMake 3.6+
+* http://wixtoolset.org[WiX Toolset] and add it to `%Path%`.
 
-NOTE: If you are using VS 2015 or later (MSVC 14.0 or later), you need to add `legacy_stdio_definitions.lib` as an additional library to odbc project linker's settings in order to be able to build the project. To add this library to the linker input in the IDE, open the context menu for the project node, choose `Properties`, then in the `Project Properties` dialog box, choose `Linker`, and edit the `Linker Input` to add `legacy_stdio_definitions.lib` to the semi-colon-separated list.
+Then, perform the following:
 
-Once the build process is complete, you can find `ignite.odbc.dll` in `%IGNITE_HOME%\platforms\cpp\project\vs\x64\Release` for the 64-bit version and in `%IGNITE_HOME%\platforms\cpp\project\vs\Win32\Release` for the 32-bit version.
-
-NOTE: Be sure to use the corresponding driver (32-bit or 64-bit) for your system.
-
-=== Building installers on Windows
-
-Once you have built driver binaries you may want to build installers for easier installation. Ignite uses link:http://wixtoolset.org[WiX Toolset] to generate ODBC installers, so to build them you'll need to download and install WiX. Make sure you have added the `bin` directory of the WiX Toolset to your PATH variable.
-
-Once everything is ready, open a terminal and navigate to the directory `%IGNITE_HOME%\platforms\cpp\odbc\install`. Execute the following commands one by one to build installers:
-
+. Navigate to the `%IGNITE_HOME%\platforms\cpp` folder.
+. Build drivers and installers using the following steps:
 
 [tabs]
 --
 tab:64-bit driver[]
 [source,shell]
 ----
-candle.exe ignite-odbc-amd64.wxs
-light.exe -ext WixUIExtension ignite-odbc-amd64.wixobj
+mkdir cmake-build-release-64
+cmake .. -DWITH_CORE=OFF -DWITH_ODBC=ON -DWITH_ODBC_MSI=ON -DCMAKE_GENERATOR_PLATFORM=x64 -DOPENSSL_ROOT_DIR=<openssl 64-bit install dir> -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=..\install\amd64
+cmake --build . --target install --config Release
 ----
 
 tab:32-bit driver[]
-[source,shell]
+[source,bat]
 ----
-candle.exe ignite-odbc-x86.wxs
-light.exe -ext WixUIExtension ignite-odbc-x86.wixobj
+mkdir cmake-build-release-32
+cmake .. -DWITH_CORE=OFF -DWITH_ODBC=ON -DWITH_ODBC_MSI=ON -DCMAKE_GENERATOR_PLATFORM=Win32 -DOPENSSL_ROOT_DIR=<openssl 32-bit install dir> -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=..\install\x86
+cmake --build . --target install --config Release
 ----
 --
 
-As a result, `ignite-odbc-amd64.msi` and `ignite-odbc-x86.msi` files should appear in the directory. You can use them to install your freshly built drivers.
+As a result, `ignite-odbc-amd64.msi` and `ignite-odbc-x86.msi` files should appear
+in `%IGNITE_HOME%\platforms\cpp\install\amd64\bin` and `%IGNITE_HOME%\platforms\cpp\install\x86\bin` directories respectively.
 
 === Building on Linux
 
-On a Linux-based operating system, you will need to install an ODBC Driver Manager of your choice to be able to build and use the Ignite ODBC Driver. The ODBC Driver has been tested with link:http://www.unixodbc.org[UnixODBC].
+On a Linux-based operating system, you will need to install an ODBC Driver Manager of your choice to be able
+to build and use the Ignite ODBC Driver. The ODBC Driver has been tested with link:http://www.unixodbc.org[UnixODBC].
 
 ==== Prerequisites
-include::includes/cpp-linux-build-prerequisites.adoc[]
+include::includes/cpp-odbc-linux-build-prerequisites.adoc[]
 
-NOTE: The JDK is used only during the build process and not by the ODBC driver itself.
 
 ==== Building ODBC driver
 - Create a build directory for cmake. We'll refer to it as `${CPP_BUILD_DIR}`
@@ -231,7 +226,7 @@
 [source,bash,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake -DCMAKE_BUILD_TYPE=Release -DWITH_ODBC=ON ${IGNITE_HOME}/platforms/cpp -DCMAKE_INSTALL_PREFIX=${CPP_INSTALL_DIR}
+cmake -DCMAKE_BUILD_TYPE=Release -DWITH_CORE=OFF -DWITH_ODBC=ON ${IGNITE_HOME}/platforms/cpp -DCMAKE_INSTALL_PREFIX=${CPP_INSTALL_DIR}
 make
 sudo make install
 ----
@@ -240,8 +235,8 @@
 [source,shell,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_ODBC=ON  ${IGNITE_HOME}/platforms/cpp -DCMAKE_INSTALL_PREFIX=${CPP_INSTALL_DIR}
-make 
+cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_CORE=OFF -DWITH_ODBC=ON  ${IGNITE_HOME}/platforms/cpp -DCMAKE_INSTALL_PREFIX=${CPP_INSTALL_DIR}
+make
 sudo make install
 ----
 
diff --git a/docs/_docs/SQL/distributed-joins.adoc b/docs/_docs/SQL/distributed-joins.adoc
index 5394c3a..34d4037 100644
--- a/docs/_docs/SQL/distributed-joins.adoc
+++ b/docs/_docs/SQL/distributed-joins.adoc
@@ -48,63 +48,4 @@
 Enable the non-colocated mode of query execution by setting a JDBC/ODBC parameter or, if you use SQL API, by calling `SqlFieldsQuery.setDistributedJoins(true)`.
 
 WARNING: If you use a non-collocated join on a column from a link:data-modeling/data-partitioning#replicated[replicated table], the column must have an index.
-Otherwise, you will get an exception.
-
-
-
-== Hash Joins
-
-//tag::hash-join[]
-To boost performance of join queries, Ignite supports the https://en.wikipedia.org/wiki/Hash_join[hash join
-algorithm].
-Hash joins can be more efficient than nested loop joins for many scenarios, except when the probe side of the join is very small.
-However, hash joins can only be used with equi-joins, i.e. a type of join with equality comparison in the join-predicate.
-
-//end::hash-join[]
-
-To enforce the use of hash joins:
-
-. Use the `enforceJoinOrder` option:
-+
-[tabs]
---
-tab:Java API[]
-[source,java]
-----
-include::{javaCodeDir}/SqlAPI.java[tags=enforceJoinOrder,indent=0]
-----
-
-tab:JDBC[]
-[source,java]
-----
-Class.forName("org.apache.ignite.IgniteJdbcThinDriver");
-
-// Open the JDBC connection.
-Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1?enforceJoinOrder=true");
-----
-tab:C#/.NET[]
-[source,csharp]
-----
-include::code-snippets/dotnet/SqlJoinOrder.cs[tag=sqlJoinOrder,indent=0]
-----
-
-tab:C++[]
-[source,c++]
-----
-include::code-snippets/cpp/src/sql_join_order.cpp[tag=sql-join-order,indent=0]
-----
---
-
-. Specify `USE INDEX(HASH_JOIN_IDX)` on the table for which you want to create the hash-join index:
-+
---
-
-[source, sql]
-----
-SELECT * FROM TABLE_A, TABLE_B USE INDEX(HASH_JOIN_IDX) WHERE TABLE_A.column1 = TABLE_B.column2
-----
---
-
-
-
-
+Otherwise, you will get an exception.
\ No newline at end of file
diff --git a/docs/_docs/clustering/baseline-topology.adoc b/docs/_docs/clustering/baseline-topology.adoc
index 4245dc7..144e41c 100644
--- a/docs/_docs/clustering/baseline-topology.adoc
+++ b/docs/_docs/clustering/baseline-topology.adoc
@@ -35,7 +35,7 @@
 Baseline topology changes automatically when <<Baseline Topology Autoadjustment>> is enabled. This is the default
 behavior for pure in-memory clusters. For persistent clusters, the baseline topology autoadjustment feature must be enabled
 manually. By default, it is disabled and you have to change the baseline topology manually. You can change the baseline
-topology using the link:control-script#activation-deactivation-and-topology-management[control script].
+topology using the link:tools/control-script#activation-deactivation-and-topology-management[control script].
 
 [CAUTION]
 ====
@@ -63,7 +63,7 @@
 
 You can activate the cluster using one of the following tools:
 
-* link:control-script#activating-cluster[Control script]
+* link:tools/control-script#activating-cluster[Control script]
 * link:restapi#change-cluster-state[REST API command]
 * Programmatically:
 +
@@ -109,7 +109,7 @@
 Baseline topology is autoadjusted only if the cluster is in the active state.
 
 To enable automatic baseline adjustment, you can use the
-link:control-script#enabling-baseline-topology-autoadjustment[control script] or the
+link:tools/control-script#enabling-baseline-topology-autoadjustment[control script] or the
 programmatic API methods shown below:
 
 [tabs]
@@ -154,6 +154,6 @@
 
 You can use the following tools to monitor and/or manage the baseline topology:
 
-* link:control-script[Control Script]
+* link:tools/control-script[Control Script]
 * link:monitoring-metrics/metrics#monitoring-topology[JMX Beans]
 
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
index d7cd753..997613c 100644
--- a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
@@ -50,11 +50,11 @@
 
         // Print the latest counter value from our counter service.
         System.out.println("Incremented value : " + counterService.get());
-        
+
         //tag::undeploy[]
         services.cancel("myCounterService");
         //end::undeploy[]
-        
+
         ignite.close();
     }
 
@@ -63,7 +63,7 @@
         //tag::deploy-with-cluster-group[]
         Ignite ignite = Ignition.start();
 
-        //deploy the service to the nodes that host the cache named "myCache" 
+        //deploy the service to the nodes that host the cache named "myCache"
         ignite.services(ignite.cluster().forCacheNodes("myCache"));
 
         //end::deploy-with-cluster-group[]
@@ -83,11 +83,11 @@
 
     @Test
     void affinityKey() {
-        
+
         //tag::deploy-by-key[]
         Ignite ignite = Ignition.start();
 
-        //making sure the cache exists
+        // Making sure the cache exists.
         ignite.getOrCreateCache("orgCache");
 
         ServiceConfiguration serviceCfg = new ServiceConfiguration();
@@ -157,6 +157,29 @@
     }
 
     @Test
+    void startWithStatistics() {
+        //tag::start-with-statistics[]
+        Ignite ignite = Ignition.start();
+
+        ServiceConfiguration serviceCfg = new ServiceConfiguration();
+
+        serviceCfg.setName("myService");
+        serviceCfg.setMaxPerNodeCount(1);
+        serviceCfg.setService(new MyCounterServiceImpl());
+
+        // Enable service statistics.
+        serviceCfg.setStatisticsEnabled(true);
+
+        ignite.services().deploy(serviceCfg);
+
+        // NOTE: work via proxy. Direct references like 'IgniteServices#service()' corrupt the statistics.
+        MyCounterService svc = ignite.services().serviceProxy("myService", MyCounterService.class, true)
+        //end::start-with-statistics[]
+
+        ignite.close();
+    }
+
+    @Test
     void serviceConfiguration() {
         //tag::service-configuration[]
         ServiceConfiguration serviceCfg = new ServiceConfiguration();
diff --git a/docs/_docs/code-snippets/k8s/stateful/node-configuration.xml b/docs/_docs/code-snippets/k8s/stateful/node-configuration.xml
index e36765d..bf7c07e 100644
--- a/docs/_docs/code-snippets/k8s/stateful/node-configuration.xml
+++ b/docs/_docs/code-snippets/k8s/stateful/node-configuration.xml
@@ -15,12 +15,14 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<!-- tag::config-block[] -->
+<?xml version="1.0" encoding="UTF-8"?>
 <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.xsd">
+       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.xsd">
 
+    <!-- tag::config-block[] -->
     <bean class="org.apache.ignite.configuration.IgniteConfiguration">
 
         <property name="workDirectory" value="/ignite/work"/>
@@ -45,8 +47,8 @@
                     <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder">
                         <constructor-arg>
                             <bean class="org.apache.ignite.kubernetes.configuration.KubernetesConnectionConfiguration">
-                                <property name="namespace" value="ignite" />
-                                <property name="serviceName" value="ignite-service" />
+                                <property name="namespace" value="default" />
+                                <property name="serviceName" value="ignite" />
                             </bean>
                         </constructor-arg>
                     </bean>
@@ -55,5 +57,5 @@
         </property>
 
     </bean>
+    <!-- end::config-block[] -->
 </beans>
-<!-- end::config-block[] -->
diff --git a/docs/_docs/code-snippets/k8s/stateless/node-configuration.xml b/docs/_docs/code-snippets/k8s/stateless/node-configuration.xml
index a694f28..a9b24b6 100644
--- a/docs/_docs/code-snippets/k8s/stateless/node-configuration.xml
+++ b/docs/_docs/code-snippets/k8s/stateless/node-configuration.xml
@@ -15,13 +15,14 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<!-- tag::config-block[] -->
+<?xml version="1.0" encoding="UTF-8"?>
 <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.xsd">
 
+    <!-- tag::config-block[] -->
     <bean class="org.apache.ignite.configuration.IgniteConfiguration">
 
         <property name="discoverySpi">
@@ -30,8 +31,8 @@
                     <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder">
                         <constructor-arg>
                             <bean class="org.apache.ignite.kubernetes.configuration.KubernetesConnectionConfiguration">
-                                <property name="namespace" value="ignite" />
-                                <property name="serviceName" value="ignite-service" />
+                                <property name="namespace" value="default" />
+                                <property name="serviceName" value="ignite" />
                             </bean>
                         </constructor-arg>
                     </bean>
@@ -39,5 +40,5 @@
             </bean>
         </property>
     </bean>
+    <!-- end::config-block[] -->
 </beans>
-<!-- end::config-block[] -->
diff --git a/docs/_docs/distributed-computing/collocated-computations.adoc b/docs/_docs/distributed-computing/collocated-computations.adoc
index 03daeb6..4fad9f1 100644
--- a/docs/_docs/distributed-computing/collocated-computations.adoc
+++ b/docs/_docs/distributed-computing/collocated-computations.adoc
@@ -66,7 +66,7 @@
 
 == Colocating by Partition
 
-The `affinityCall(Collection<String> cacheNames, int partId, IgniteRunnable job)` and `affinityRun(Collection<String> cacheNames, int partId, IgniteRunnable job)` send a given task to the node where the partition with a given ID is located. This is useful when you need to retrieve objects for multiple keys and you know that the keys belong to the same partition. In this case, you can create one task instead of multiple task for each key.
+The `affinityCall(Collection<String> cacheNames, int partId, IgniteCallable job)` and `affinityRun(Collection<String> cacheNames, int partId, IgniteRunnable job)` send a given task to the node where the partition with a given ID is located. This is useful when you need to retrieve objects for multiple keys and you know that the keys belong to the same partition. In this case, you can create one task instead of multiple task for each key.
 
 For example, let's say you want to calculate the arithmetic mean of a specific field for a specific subset of keys.
 If you want to distribute the computation, you can group the keys by partitions and send each group of keys to the node where the partition is located to get the values.
diff --git a/docs/_docs/extensions-and-integrations/hibernate-l2-cache.adoc b/docs/_docs/extensions-and-integrations/hibernate-l2-cache.adoc
index b054de9..8570df9 100644
--- a/docs/_docs/extensions-and-integrations/hibernate-l2-cache.adoc
+++ b/docs/_docs/extensions-and-integrations/hibernate-l2-cache.adoc
@@ -35,7 +35,7 @@
 * cache transactions, that make `TRANSACTIONAL` mode possible.
 * clustering, with 2 different replication modes: `REPLICATED` and `PARTITIONED`
 
-To start using GridGain as a Hibernate L2 cache, you need to perform 3 simple steps:
+To start using Ignite as a Hibernate L2 cache, you need to perform 3 simple steps:
 
 * Add Ignite libraries to your application's classpath.
 * Enable L2 cache and specify Ignite implementation class in L2 cache configuration.
@@ -233,7 +233,7 @@
 [NOTE]
 ====
 The nodes may be started on other hosts as well, forming a distributed caching cluster.
-Be sure to specify the right network settings in GridGain configuration file for that.
+Be sure to specify the right network settings in Ignite configuration file for that.
 ====
 
 == Query Cache
diff --git a/docs/_docs/extensions-and-integrations/spring/spring-data.adoc b/docs/_docs/extensions-and-integrations/spring/spring-data.adoc
index 804d0e9..b23db40 100644
--- a/docs/_docs/extensions-and-integrations/spring/spring-data.adoc
+++ b/docs/_docs/extensions-and-integrations/spring/spring-data.adoc
@@ -287,7 +287,7 @@
 
 == Example
 
-The complete example is available on link: https://github.com/apache/ignite-extensions/tree/master/modules/spring-data-2.0-ext/examples/main[GitHub, windows="_blank"]
+The complete example is available on link:https://github.com/apache/ignite-extensions/tree/master/modules/spring-data-2.0-ext/examples[GitHub, windows="_blank"]
 
 == Tutorial
 
diff --git a/docs/_docs/includes/cpp-odbc-linux-build-prerequisites.adoc b/docs/_docs/includes/cpp-odbc-linux-build-prerequisites.adoc
new file mode 100644
index 0000000..1bf34b7
--- /dev/null
+++ b/docs/_docs/includes/cpp-odbc-linux-build-prerequisites.adoc
@@ -0,0 +1,43 @@
+// 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.
+The following packages need to be installed:
+
+- C++ compiler
+- cmake 3.6+
+- openssl, including header files
+- unixODBC
+
+Installation instructions for several popular distributions are listed below:
+[tabs]
+--
+tab:Ubuntu 18.04/20.04[]
+[source,bash,subs="attributes,specialchars"]
+----
+sudo apt-get install -y build-essential cmake unixodbc-dev libssl-dev
+----
+
+tab:CentOS/RHEL 7[]
+[source,shell,subs="attributes,specialchars"]
+----
+sudo yum install -y epel-release
+sudo yum install -y cmake3 unixODBC-devel openssl-devel make gcc-c++
+----
+
+tab:CentOS/RHEL 8[]
+[source,shell,subs="attributes,specialchars"]
+----
+sudo yum install -y cmake3 unixODBC-devel openssl-devel make gcc-c++
+----
+--
diff --git a/docs/_docs/includes/starting-node.adoc b/docs/_docs/includes/starting-node.adoc
index b22a25a..824b6e5 100644
--- a/docs/_docs/includes/starting-node.adoc
+++ b/docs/_docs/includes/starting-node.adoc
@@ -27,7 +27,7 @@
 cd {IGNITE_HOME}/bin/
 ----
 
-tab:Window[]
+tab:Windows[]
 [source,shell]
 ----
 cd {IGNITE_HOME}\bin\
@@ -48,7 +48,7 @@
 ./ignite.sh ../examples/config/example-ignite.xml
 ----
 
-tab:Window[]
+tab:Windows[]
 [source,shell]
 ----
 ignite.bat ..\examples\config\example-ignite.xml
diff --git a/docs/_docs/key-value-api/transactions.adoc b/docs/_docs/key-value-api/transactions.adoc
index ab208eb..a6de3e6 100644
--- a/docs/_docs/key-value-api/transactions.adoc
+++ b/docs/_docs/key-value-api/transactions.adoc
@@ -106,8 +106,7 @@
 
 In `PESSIMISTIC` transactions, locks are acquired during the first read or write access (depending on the isolation level) and held by the transaction until it is committed or rolled back. In this mode locks are acquired on primary nodes first and then promoted to backup nodes during the prepare stage. The following isolation levels can be configured with the `PESSIMISTIC` concurrency mode:
 
-* `READ_COMMITTED` - Data is read without a lock and is never cached in the transaction itself. The data may be read from a backup node if this is allowed in the cache configuration. In this isolation mode you can have the so-called Non-Repeatable Reads
-* because a concurrent transaction can change the data when you are reading the data twice in your transaction. The lock is only acquired at the time of first write access (this includes `EntryProcessor` invocation). This means that an entry that has been read during the transaction may have a different value by the time the transaction is committed. No exception is thrown in this case.
+* `READ_COMMITTED` - Data is read without a lock and is never cached in the transaction itself. The data may be read from a backup node if this is allowed in the cache configuration. In this isolation mode you can have the so-called Non-Repeatable Reads because a concurrent transaction can change the data when you are reading the data twice in your transaction. The lock is only acquired at the time of first write access (this includes `EntryProcessor` invocation). This means that an entry that has been read during the transaction may have a different value by the time the transaction is committed. No exception is thrown in this case.
 
 * `REPEATABLE_READ` - Entry lock is acquired and data is fetched from the primary node on the first read or write access and stored in the local transactional map. All consecutive access to the same data is local and returns the last read or updated transaction value. This means no other concurrent transactions can make changes to the locked data, and you are getting Repeatable Reads for your transaction.
 
diff --git a/docs/_docs/monitoring-metrics/metrics.adoc b/docs/_docs/monitoring-metrics/metrics.adoc
index 2725d7e..3637b43 100644
--- a/docs/_docs/monitoring-metrics/metrics.adoc
+++ b/docs/_docs/monitoring-metrics/metrics.adoc
@@ -429,10 +429,6 @@
 --
 ////
 
-== Monitoring Data Center Replication
-
-Refer to the link:data-center-replication/managing-and-monitoring#dr_jmx[Managing and Monitoring Replication] page.
-
 
 ////
 == Monitoring Memory Consumption
diff --git a/docs/_docs/monitoring-metrics/new-metrics.adoc b/docs/_docs/monitoring-metrics/new-metrics.adoc
index 08b69c7..44a68fc 100644
--- a/docs/_docs/monitoring-metrics/new-metrics.adoc
+++ b/docs/_docs/monitoring-metrics/new-metrics.adoc
@@ -452,3 +452,17 @@
 |TotalClientNodes| integer | Client nodes count.
 |TotalServerNodes| integer | Server nodes count.
 |===
+
+== Cache processor
+
+Cache processor metrics.
+
+Register name: `cache`
+
+
+[cols="2,1,3",opts="header"]
+|===
+|Name|    Type|    Description
+|LastDataVer| long | The latest data version on the node.
+|DataVersionClusterId| integer | Data version cluster id.
+|===
diff --git a/docs/_docs/monitoring-metrics/system-views.adoc b/docs/_docs/monitoring-metrics/system-views.adoc
index c642aad..96d9cba 100644
--- a/docs/_docs/monitoring-metrics/system-views.adoc
+++ b/docs/_docs/monitoring-metrics/system-views.adoc
@@ -114,6 +114,7 @@
 |EVICTION_FILTER | string |  toString representation of eviction filter
 |EVICTION_POLICY_FACTORY | string |  toString representation of eviction policy factory
 |EXPIRY_POLICY_FACTORY | string |  toString representation of expiry policy factory
+|CONFLICT_RESOLVER | string |  toString representation of cache conflict resolver
 |INTERCEPTOR | string |  toString representation of interceptor
 |IS_COPY_ON_READ | boolean | Flag indicating whether a copy of the value stored in the on-heap cache
 |IS_EAGER_TTL | boolean | Flag indicating whether expired cache entries will be eagerly removed from cache
@@ -949,3 +950,16 @@
 |VERSION |BIGINT |Statistics version.
 |LAST_UPDATE_TIME |VARCHAR |Maximum time of all partition statistics which was used to generate local one.
 |===
+
+== SNAPSHOT
+
+The SNAPSHOT view exposes information about local snapshots.
+
+[{table_opts}]
+|===
+| Column | Data Type | Description
+| NAME | VARCHAR | Snapshot name.
+| CONSISTENT_ID | VARCHAR | Consistent ID of a node to which snapshot data relates.
+| BASELINE_NODES | VARCHAR | Baseline nodes affected by the snapshot.
+| CACHE_GROUPS | VARCHAR | Cache group names that were included in the snapshot.
+|===
diff --git a/docs/_docs/perf-and-troubleshooting/thread-pools-tuning.adoc b/docs/_docs/perf-and-troubleshooting/thread-pools-tuning.adoc
index 6da456c..b37fd0f 100644
--- a/docs/_docs/perf-and-troubleshooting/thread-pools-tuning.adoc
+++ b/docs/_docs/perf-and-troubleshooting/thread-pools-tuning.adoc
@@ -60,6 +60,14 @@
 
 The default pool size is `max(8, total number of cores)`. Use `IgniteConfiguration.setDataStreamerThreadPoolSize(...)` or a similar API from your programming language to change the pool size.
 
+== Snapshot Pool
+
+The snapshot pool is used for processing all the cluster operations related to taking or restoring Apache Ignite snapshots.
+
+The default pool size is `4` (see the `IgniteConfiguration.DFLT_SNAPSHOT_THREAD_POOL_SIZE`).
+Use `IgniteConfiguration.setSnapshotThreadPoolSize(...)` to change the pool size.
+
+
 == Creating Custom Thread Pool
 
 It is possible to configure a custom thread pool for compute tasks.
diff --git a/docs/_docs/persistence/change-data-capture.adoc b/docs/_docs/persistence/change-data-capture.adoc
new file mode 100644
index 0000000..a5e0fb2
--- /dev/null
+++ b/docs/_docs/persistence/change-data-capture.adoc
@@ -0,0 +1,132 @@
+// 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.
+= Change Data Capture
+
+
+== Overview
+Change Data Capture (link:https://en.wikipedia.org/wiki/Change_data_capture[CDC]) is a data processing pattern used to asynchronously receive entries that have been changed on the local node so that action can be taken using the changed entry.
+
+WARNING: CDC is an experimental feature whose API or design architecture might be changed.
+
+Below are some of the CDC use cases:
+
+ * Streaming changes in Warehouse;
+ * Updating search index;
+ * Calculating statistics (streaming queries);
+ * Auditing logs;
+ * Async interaction with extenal system: Moderation, business process invocation, etc.
+
+Ignite implements CDC with the `ignite-cdc.sh` application and link:https://github.com/apache/ignite/blob/master/modules/core/src/main/java/org/apache/ignite/cdc/CdcConsumer.java#L56[Java API].
+
+Below are the CDC application and the Ignite node integrated via WAL archive segments:
+
+image:../../assets/images/integrations/CDC-design.svg[]
+
+When CDC is enabled, the Ignite server node creates a hard link to each WAL archive segment in the special `db/cdc/\{consistency_id\}` directory.
+The `ignite-cdc.sh` application runs on a different JVM and processes newly archived link:native-persistence.adoc#_write-ahead_log[WAL segments].
+When the segment is fully processed by `ignite-cdc.sh`, it is removed. The actual disk space is free when both links (archive and CDC) are removed.
+
+State of consumption is a pointer to the last processed event.
+Consumer can tell to `ignite-cdc.sh` to save the consumption state.
+On startup event processing will be continued from the last saved state.
+
+== Configuration
+
+=== Ignite Node
+
+[cols="20%,45%,35%",opts="header"]
+|===
+|Name |Description | Default value
+| `DataStorageConfiguration#cdcEnabled` | Flag to enable CDC on the server node. | `false`
+| `DataStorageConfiguration#cdcWalPath` | Path to the CDC directory | `"db/wal/cdc"`
+| `DataStorageConfiguration#walForceArchiveTimeout` | Timeout to forcefully archive the WAL segment even it is not complete. | `-1` (disabled)
+|===
+
+=== CDC Application
+
+CDC is configured in the same way as the Ignite node - via the spring XML file:
+
+* `ignite-cdc.sh` requires both Ignite and CDC configurations to start;
+* `IgniteConfiguration` is used to determine common options like a path to the CDC directory, node consistent id, and other parameters;
+* `CdcConfiguration` contains `ignite-cdc.sh`-specific options.
+
+[cols="20%,45%,35%",opts="header"]
+|===
+|Name |Description | Default value
+| `lockTimeout` | Timeout to wait for lock acquiring. CDC locks directory on a startup to ensure there is no concurrent `ignite-cdc.sh` processing the same directory.
+| 1000 milliseconds.
+| `checkFrequency` | Amount of time application sleeps between subsequent checks when no new files available. | 1000 milliseconds.
+| `keepBinary` | Flag to specify if key and value of changed entries should be provided in link:../key-value-api/binary-objects.adoc[binary format]. | `true`
+| `consumer` | Implementation of `org.apache.ignite.cdc.CdcConsumer` that consumes entries changes. | null
+| `metricExporterSpi` | Array of SPI's to export CDC metrics. See link:../monitoring-metrics/new-metrics-system.adoc#_metric_exporters[metrics] documentation, also. | null
+|===
+
+== API
+
+=== `org.apache.ignite.cdc.CdcEvent`
+
+Below is a single change of the data reflected by `CdcEvent`. 
+
+[cols="20%,80%",opts="header"]
+|===
+|Name |Description
+| `key()` | Key for the changed entry.
+| `value()` | Value for the changed entry. This method will return `null` if the event reflects removal.
+| `cacheId()` | ID of the cache where the change happens. The value is equal to the `CACHE_ID` from link:../monitoring-metrics/system-views.adoc#_CACHES[`SYS.CACHES`].
+| `partition()` | Partition of the changed entry.
+| `primary()` | Flag to distinguish if operation happens on the primary or a backup node.
+| `version()` | `Comparable` version of the changed entry. Internally, Ignite maintains ordered versions of each entry so any changes of the same entry can be sorted.
+|===
+
+=== `org.apache.ignite.cdc.CdcConsumer`
+
+The consumer of change events.  It should be implemented by the user.
+[cols="20%,80%",opts="header"]
+|===
+|Name |Description
+| `void start(MetricRegistry)` | Invoked one-time at the start of the CDC application. `MetricRegistry` should be used to export the consumer-specific metrics.
+| `boolean onEvents(Iterator<CdcEvent> events)` | The main method that processes changes. When this method returns `true`, the state is saved on the disk. State points to the event next to the last read event. In case of any failure, consumption will continue from the last saved state.
+| `void stop()` | Invokes one-time at the stop of the CDC application.
+|===
+
+== Metrics
+
+`ignite-cdc.sh` uses the same SPI to export metrics as Ignite does.
+The following metrics are provided by the application (additional metrics can be provided by the consumer):
+|===
+|Name |Description
+| CurrentSegmentIndex | Index of the currently processing WAL segment.
+| CommittedSegmentIndex | Index of the WAL segment that contains the last committed state.
+| CommittedSegmentOffset | Committed offset in bytes inside the WAL segment.
+| LastSegmentConsumptionTime | Timestamp (in milliseconds) indicating the last segment processing start.
+| BinaryMetaDir | Binary meta-directory the application reads data from.
+| MarshallerDir | Marshaller directory the application reads data from.
+| CdcDir | The CDC directory the application reads data from.
+|===
+
+== Logging
+
+`ignite-cdc.sh` uses the same logging configuration as the Ignite node does. The only difference is that the log is written in the"ignite-cdc.log" file.
+
+== Lifecycle
+
+IMPORTANT: `ignite-cdc.sh` implements the fail-fast approach. It just fails in case of any error. The restart procedure should be configured with the OS tools.
+
+ 1. Find the required shared directories. Take the values from the provided `IgniteConfiguration`.
+ 2. Lock the CDC directory.
+ 3. Load the saved state.
+ 4. Start the consumer.
+ 5. Infinitely wait for the newly available segment and process it.
+ 6. Stop the consumer in case of a failure or a received stop signal.
\ No newline at end of file
diff --git a/docs/_docs/persistence/native-persistence.adoc b/docs/_docs/persistence/native-persistence.adoc
index 14e9e6a..f0ddbb7 100644
--- a/docs/_docs/persistence/native-persistence.adoc
+++ b/docs/_docs/persistence/native-persistence.adoc
@@ -33,6 +33,7 @@
 * Storing data partitions on disk
 * Write-ahead logging
 * Checkpointing
+* link:persistence/change-data-capture[Change Data Capture]
 * Usage of OS swap
 ////
 *TODO: diagram: update operation + wal + checkpointing*
diff --git a/docs/_docs/quick-start/cpp.adoc b/docs/_docs/quick-start/cpp.adoc
index cdc3306..08aa6df 100644
--- a/docs/_docs/quick-start/cpp.adoc
+++ b/docs/_docs/quick-start/cpp.adoc
@@ -41,15 +41,39 @@
 Ignite ships with a robust {cpp} client.
 To get started with Ignite and {cpp}, you will need to be familiar with building {cpp} applications.
 
-. Install `openssl` and add it to your path.
 . If you haven't already, download/install <<Installing Ignite,Apache Ignite>>.
-. Navigate to the `{IGNITE_HOME}/platforms/cpp/project/vs` folder.
-. Launch the appropriate Visual Studio solution file for your system (`ignite.sln` is for 64-bit).
-. Build the solution.
+. Install `CMake 3.6+` and add it to the `%Path%`.
+. Install `OpenSSL`.
+. Set `%JAVA_HOME%` environment variable.
+. Navigate to the `%IGNITE_HOME%\platforms\cpp\` folder.
+. Create build directory `%IGNITE_HOME%\platforms\cpp\cmake-build-release`.
+. Build and install `Ignite C++`.
 
-From here, you can create your own code, or run one of the existing examples located in the `{IGNITE_HOME}/platforms/cpp/examples/project/vs` directory.
+[tabs]
+--
+tab:Win32[]
+[source,bat,subs="attributes,specialchars"]
+----
+cd cmake-build-release
+cmake .. -DWITH_THIN_CLIENT=ON -DCMAKE_GENERATOR_PLATFORM=Win32 -DOPENSSL_ROOT_DIR=<openssl install dir> -DCMAKE_INSTALL_PREFIX=<ignite cpp install dir>
+cmake --build . --target install  --config Release
+----
 
-There is much more information about how to build, test, and use GGCE for {cpp} in the `readme.txt` and `DEVNOTES.txt` files located in the `{IGNITE_HOME}/platforms/cpp` folder.
+tab:Win64[]
+[source,bat,subs="attributes,specialchars"]
+----
+cd cmake-build-release
+cmake .. -DWITH_THIN_CLIENT=ON -DCMAKE_GENERATOR_PLATFORM=x64 -DOPENSSL_ROOT_DIR=<openssl install dir> -DCMAKE_INSTALL_PREFIX=<ignite cpp install dir>
+cmake --build . --target install  --config Release
+----
+--
+`CMake` by default generates on Windows Visual Studio projects. You can find generated projects in CMake
+build directory and open `Ignite.C++.sln` in Visual Studio.
+
+From here, you can create your own code, or run one of the existing examples located in the `{IGNITE_HOME}/platforms/cpp/examples/` directory.
+
+There is much more information about how to build, test, and use Apache Ignite for {cpp} in the `README.txt` and
+`DEVNOTES.txt` files located in the `{IGNITE_HOME}/platforms/cpp` folder.
 
 For information about the {cpp} thin client, see link:thin-clients/cpp-thin-client[C++ Thin Client].
 
@@ -72,7 +96,7 @@
 [source,bash,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake -DCMAKE_BUILD_TYPE=Release -DWITH_ODBC=ON -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp 
+cmake -DCMAKE_BUILD_TYPE=Release -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp
 make
 sudo make install
 ----
@@ -81,8 +105,8 @@
 [source,shell,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_ODBC=ON -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp 
-make 
+cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp
+make
 sudo make install
 ----
 
diff --git a/docs/_docs/quick-start/java.adoc b/docs/_docs/quick-start/java.adoc
index 9f4f3ca..4d2c469 100644
--- a/docs/_docs/quick-start/java.adoc
+++ b/docs/_docs/quick-start/java.adoc
@@ -22,7 +22,7 @@
 
 include::includes/prereqs.adoc[]
 
-If you use Java version 11 or later, see <<Running Ignite with Java 11 or later>> for details.
+If you use Java version 11 or later, see <<Running Ignite with Java 11>> for details.
 
 == Installing Ignite
 
diff --git a/docs/_docs/services/services.adoc b/docs/_docs/services/services.adoc
index dfdf114..6943097 100644
--- a/docs/_docs/services/services.adoc
+++ b/docs/_docs/services/services.adoc
@@ -261,6 +261,35 @@
 
 In this way, you don't have to stop the server nodes, so you don't interrupt the operation of your cluster.
 
+== Service Statistics
+
+You can measure durations of your service's methods. If you want this analytics, enable service statistics in the
+service configuration. Service statistics are collected under name "Services" in
+link:monitoring-metrics/new-metrics-system.adoc[metrics], in
+link:monitoring-metrics/system-views.adoc[system views], and in JMX.
+
+[tabs]
+--
+tab:Java[]
+[source, java]
+----
+include::{javaFile}[tags=start-with-statistics, indent=0]
+----
+tab:C#/.NET[]
+tab:C++[]
+--
+
+[NOTE]
+====
+You should be aware:
+
+1. Direct references to the services like `IgniteServices.service(name)` corrupt the statistics. Use the proxies
+instead (`IgniteServices.serviceProxy(...)`).
+2. Overloaded service methods have the same metric by the method name.
+3. Service statistics do not take in account serialization and network issues.
+4. Service statistics slows down the invocations of service methods. It's not
+an issue for real jobs like working with a DB or a cache.
+====
 
 // TODO: add how to  call java services from .NET
 
diff --git a/docs/_docs/setup.adoc b/docs/_docs/setup.adoc
index fb77e97..47a83b2 100644
--- a/docs/_docs/setup.adoc
+++ b/docs/_docs/setup.adoc
@@ -162,7 +162,7 @@
 the following methods:
 
 * If you use the binary distribution, move the
-`lib/optional/{module-dir}` to the `lib` directory before starting the
+`lib/optional/{module-dir}` to the `libs` directory before starting the
 node.
 * Add libraries from `lib/optional/{module-dir}` to the classpath of
 your application.
diff --git a/docs/_docs/snapshots/snapshots.adoc b/docs/_docs/snapshots/snapshots.adoc
index 91df5c3..7b0e5f8 100644
--- a/docs/_docs/snapshots/snapshots.adoc
+++ b/docs/_docs/snapshots/snapshots.adoc
@@ -76,16 +76,25 @@
 The link:persistence/snapshots#restoring-from-snapshot[restore procedure] explains how to tether together all the segments during recovery.
 ====
 
-== Configuring Snapshot Directory
+== Configuration
+
+=== Snapshot Directory
 
 By default, a segment of the snapshot is stored in the work directory of a respective Ignite node and uses the same storage
 media where Ignite Persistence keeps data, index, WAL, and other files. Since the snapshot can consume as much space as
-already taken by the persistence files and can affect your applications' performance by sharing the disk I/O with the
+already taken by the persistence files and can affect your application's performance by sharing the disk I/O with the
 Ignite Persistence routines, it's suggested to store the snapshot and persistence files on different media.
 
 See the link:persistence/snapshot-directory#configuring-snapshot-directory[Configuring Snapshot Directory] page for
 configuration examples.
 
+=== Snapshot Execution Pool
+
+By default, the snapshot thread pool size has a value of `4`. Decreasing the number of threads involved in the snapshot creation process 
+increases the total amount of time for taking a snapshot. However, this keeps the disk load within reasonable limits.
+
+See the link:perf-and-troubleshooting/thread-pools-tuning[Ignite Snapshot Execution Pool,window=_blank] page for more details.
+
 == Creating Snapshot
 
 Ignite provides several APIs for the snapshot creation. Let's review all the options.
@@ -247,8 +256,6 @@
 * Encrypted caches are not included in the snapshot.
 * You can have only one snapshotting operation running at a time.
 * The snapshot procedure is interrupted if a server node leaves the cluster.
-* Snapshot may be restored only at the same cluster topology with the same node IDs;
-* The automatic restore procedure is not available yet. You have to restore it manually.
 
 If any of these limitations prevent you from using Apache Ignite, then select alternate snapshotting implementations for
 Ignite provided by enterprise vendors.
diff --git a/docs/_docs/thin-clients/cpp-thin-client.adoc b/docs/_docs/thin-clients/cpp-thin-client.adoc
index 8bb5612..cc69237 100644
--- a/docs/_docs/thin-clients/cpp-thin-client.adoc
+++ b/docs/_docs/thin-clients/cpp-thin-client.adoc
@@ -17,7 +17,8 @@
 == Prerequisites
 
 * C++ compiler: MS Visual C++ (10.0 and up), g++ (4.4.0 and up)
-* Visual Studio 2010 or newer
+* OpenSSL
+* CMake 3.6+
 
 
 == Installation
@@ -26,19 +27,31 @@
 
 [tabs]
 --
-tab:Windows[]
+tab:Win64[]
 [source,bat]
 ----
-cd %IGNITE_HOME%\platforms\cpp\project\vs
+cd %IGNITE_HOME%\platforms\cpp\
+mkdir cmake-build-release
+cd cmake-build-release
+cmake .. -DWITH_CORE=OFF -DWITH_THIN_CLIENT=ON -DCMAKE_GENERATOR_PLATFORM=x64 -DOPENSSL_ROOT_DIR=<openssl install dir> -DCMAKE_INSTALL_PREFIX=<ignite cpp install dir>
+cmake --build . --target install --config Release
+----
 
-msbuild ignite.sln /p:Configuration=Release /p:Platform=x64
+tab:Win32[]
+[source,bat]
+----
+cd %IGNITE_HOME%\platforms\cpp\
+mkdir cmake-build-release
+cd cmake-build-release
+cmake .. -DWITH_CORE=OFF  -DWITH_THIN_CLIENT=ON -DCMAKE_GENERATOR_PLATFORM=Win32 -DOPENSSL_ROOT_DIR=<openssl install-dir> -DCMAKE_INSTALL_PREFIX=<ignite cpp install dir>
+cmake --build . --target install --config Release
 ----
 
 tab:Ubuntu[]
 [source,bash,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake -DCMAKE_BUILD_TYPE=Release -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp 
+cmake -DCMAKE_BUILD_TYPE=Release -DWITH_CORE=OFF -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp
 make
 sudo make install
 ----
@@ -47,8 +60,8 @@
 [source,shell,subs="attributes,specialchars"]
 ----
 cd ${CPP_BUILD_DIR}
-cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp 
-make 
+cmake3 -DCMAKE_BUILD_TYPE=Release -DWITH_CORE=OFF -DWITH_THIN_CLIENT=ON ${IGNITE_HOME}/platforms/cpp
+make
 sudo make install
 ----
 
diff --git a/docs/_docs/thin-clients/java-thin-client.adoc b/docs/_docs/thin-clients/java-thin-client.adoc
index 2f71720..cfa9f83 100644
--- a/docs/_docs/thin-clients/java-thin-client.adoc
+++ b/docs/_docs/thin-clients/java-thin-client.adoc
@@ -241,7 +241,7 @@
 
 * Get or change the state of a cluster
 * Get a list of all cluster nodes
-* Create logical groups our of cluster nodes and use other Ignite APIs to perform certain operations on the group
+* Create logical groups of cluster nodes and use other Ignite APIs to perform certain operations on the group
 
 Use the instance of `IgniteClient` to obtain a reference to the `ClientCluster` interface:
 [source, java]
diff --git a/docs/_docs/tools/control-script.adoc b/docs/_docs/tools/control-script.adoc
index be2f9ed..3538eb6 100644
--- a/docs/_docs/tools/control-script.adoc
+++ b/docs/_docs/tools/control-script.adoc
@@ -593,7 +593,7 @@
 [source,text]
 ----
 Control utility [ver. 2.10.0]
-2021 Copyright(C) Apache Software Foundation
+2022 Copyright(C) Apache Software Foundation
 User: test
 Time: 2021-04-27T16:13:21.213
 Command [CACHE] started
@@ -1025,6 +1025,7 @@
 ----
 control.bat --property get --name 'statistics.usage.state'
 ----
+--
 
 == Cache Consistency
 
@@ -1037,12 +1038,12 @@
 tab:Unix[]
 [source,shell]
 ----
-control.sh --enable-experimental --consistency repair cache-name partition
+control.sh --enable-experimental --consistency repair --cache cache-name --partition partition --strategy strategy
 ----
 tab:Window[]
 [source,shell]
 ----
-control.bat --enable-experimental --consistency repair cache-name partition
+control.bat --enable-experimental --consistency repair --cache cache-name --partition partition --strategy strategy
 ----
 --
 Parameters:
@@ -1050,8 +1051,9 @@
 [cols="1,3",opts="header"]
 |===
 | Parameter | Description
-| `cache-name`| Cache to be checked/repaired..
+| `cache-name`| Cache to be checked/repaired.
 | `partition`| Cache's partition to be checked/repaired.
+| `strategy`| Repair strategy [LWW, PRIMARY, RELATIVE_MAJORITY, REMOVE, CHECK_ONLY].
 |===
 
 === Status
diff --git a/docs/assets/images/integrations/CDC-design.svg b/docs/assets/images/integrations/CDC-design.svg
new file mode 100644
index 0000000..d314d42
--- /dev/null
+++ b/docs/assets/images/integrations/CDC-design.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Do not edit this file with editors other than diagrams.net -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="391px" height="331px" viewBox="-0.5 -0.5 391 331" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2021-12-29T14:25:17.176Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.85 YaBrowser/21.11.0.2054 Yowser/2.5 Safari/537.36&quot; etag=&quot;drvwgZC5R8PPEA_YrwKP&quot; version=&quot;15.7.0&quot; type=&quot;google&quot;&gt;&lt;diagram id=&quot;J1pRnCWok6TMOavkrk-d&quot; name=&quot;Page-1&quot;&gt;5VnJcts4FPwaHc3CyuVoy5rkkJSTqFIen6ZoEiZRpggVCVlSvn4eRXAFZSvj0diT+GKiATwS3Y2HRTM6X+0+FOE6/axikc0Iinczej0jBGPqwb8K2deIx1ENJIWMTaMOWMofwoBNs42MRTloqJXKtFwPwUjluYj0AAuLQm2HzR5UNnzrOkyEBSyjMLPRWxnrtEZ94nX4RyGTtHkzdoO6ZhU2jc1IyjSM1bYH0cWMzguldP202s1FVpHX8FL3++NIbfthhcj1KR3Q5qbcfL9dPC0fbvz8+/rbnfpxYaI8hdnGDNh8rN43DCSF2qxNM1FosZviPbxvmiP7u3A7WrCJUCuhiz00aT1iuhiHNBG2Hd00MFjao5pSA4ZG4qQN3bEAD4aIaVIWiaeRf/khXwZ/8bt89bXMPl6Ql0kBTvJYVEHwjF5tU6nFch1GVe0WJgJgqV5lpnpCqee0GJNsM/d21FCLGpnkMHinTN8ZR6aWUQcF/T82MBvHDnZtvyHXCWxame/47EzEMovY28tPFqWQQ9bVY7TPJHBbkJeJva9V+HTfAmH0mBy0udloCCMMXta5F/PzqoERooxBAuW+2ySRfZMJAgcU8lyPMey7iPmWMpgwB9aUgGITxbVlwog4lHDuYxpwynyfn0kz/t7zhKnlqDV5k2P5RJJFQWvvPp38XLnEPZJLLqI4msonMFo9JKzUhXoUc5WpApBcVV6+epBZNoLCDCJX0waIFYBfVdxJWOQvTcVKxnH1mkmFOg3RGUXCxHNYgFw/cDnHHmFkqJhLHHB2QBGpvW1PDc6dql87PSamBuFONy2oBy89k7aepe1SABcA5bBFrL5Klfo3U5i4jkv7Cg2TH3E8kL/NfrbAlLtO644qxhvq61v6Xs8rRtA8DfMENuzHVq5c6WMq9BetI2tUHBaPN9BL6oo15KBzLlaEU4fxkUh23vQnNmCwyJ2J+OB3IN61iadvTXxzShnv0OCUGaXySfwSxHs28ezNiT/hjCry+LI67FcrQBaWpYyG7Iqd1H8e+IP9pSnfHcqMN+Xrncn/h8K+V/giCgljqdaVGsthXHU4hNwGMPE80gBdwENp3y+NQ/6coqXaFJF4+TShwyIR+uU0LuLBJchRf1xMbBv5hBme2zKa4F+UhJH2nDe8BKDuKEQ9ZNOrf8sxCkTJKBAeBao5sQIdrNmO+BVuPeHy4HS38p5X8fM+BQmX5hWq0KlKVB5miw79ty1GT7QYezcWC0bOGIc42WJsFGic+s5tMfsS5hUWYwEfJEQ4nr8uIdJhQqyOKv8gIf7nfub/t5SJR35uU+jP+pmM/NzewL7az1Ds7rfr5t2vBHTxNw==&lt;/diagram&gt;&lt;/mxfile&gt;"><defs/><g><rect x="0" y="0" width="390" height="330" rx="49.5" ry="49.5" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" pointer-events="none"/><rect x="43.1" y="51.16" width="306.9" height="48.84" rx="7.33" ry="7.33" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 305px; height: 1px; padding-top: 76px; margin-left: 44px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">ignite.sh</div></div></div></foreignObject><text x="197" y="79" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">ignite.sh</text></switch></g><path d="M 43.1 135 C 43.1 126.72 70.89 120 105.17 120 C 121.63 120 137.42 121.58 149.06 124.39 C 160.7 127.21 167.24 131.02 167.24 135 L 167.24 207.33 C 167.24 215.61 139.45 222.33 105.17 222.33 C 70.89 222.33 43.1 215.61 43.1 207.33 Z" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 167.24 135 C 167.24 143.28 139.45 150 105.17 150 C 70.89 150 43.1 143.28 43.1 135" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 122px; height: 1px; padding-top: 171px; margin-left: 44px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">WAL</div></div></div></foreignObject><text x="105" y="175" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">WAL</text></switch></g><rect x="50.16" y="250" width="309.84" height="50" rx="7.5" ry="7.5" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 153px; height: 1px; padding-top: 275px; margin-left: 128px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">ignite-cdc.sh</div></div></div></foreignObject><text x="205" y="279" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">ignite-cdc.sh</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 355px; height: 1px; padding-top: 26px; margin-left: 28px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">Sever node host</div></div></div></foreignObject><text x="205" y="29" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">Sever node host</text></switch></g><path d="M 253.45 120 L 303.45 120 L 333.45 150 L 333.45 220 L 253.45 220 L 253.45 120 Z" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 303.45 120 L 303.45 150 L 333.45 150 Z" fill-opacity="0.05" fill="#000000" stroke="none" pointer-events="none"/><path d="M 303.45 120 L 303.45 150 L 333.45 150" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 170px; margin-left: 254px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">DC-1 Changes</div></div></div></foreignObject><text x="293" y="174" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">DC-1 Changes</text></switch></g><path d="M 263.45 130 L 313.45 130 L 343.45 160 L 343.45 230 L 263.45 230 L 263.45 130 Z" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 313.45 130 L 313.45 160 L 343.45 160 Z" fill-opacity="0.05" fill="#000000" stroke="none" pointer-events="none"/><path d="M 313.45 130 L 313.45 160 L 343.45 160" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 180px; margin-left: 264px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">DC-1 Changes</div></div></div></foreignObject><text x="303" y="184" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">DC-1 Changes</text></switch></g><path d="M 273.45 140 L 323.45 140 L 353.45 170 L 353.45 240 L 273.45 240 L 273.45 140 Z" fill="rgba(255, 255, 255, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 323.45 140 L 323.45 170 L 353.45 170 Z" fill-opacity="0.05" fill="#000000" stroke="none" pointer-events="none"/><path d="M 323.45 140 L 323.45 170 L 353.45 170" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 190px; margin-left: 274px;"><div data-drawio-colors="color: rgba(0, 0, 0, 1); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">WAL archive</div></div></div></foreignObject><text x="313" y="194" fill="rgba(0, 0, 0, 1)" font-family="Helvetica" font-size="12px" text-anchor="middle">WAL archive</text></switch></g><path d="M 167.11 166.97 L 247.56 167.18" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 252.81 167.2 L 245.8 170.68 L 247.56 167.18 L 245.82 163.68 Z" fill="rgba(0, 0, 0, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 196.55 100 L 196.58 105 Q 196.6 110 186.6 110 L 115.1 110 Q 105.1 110 105.1 111.82 L 105.1 113.63" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 105.1 118.88 L 101.6 111.88 L 105.1 113.63 L 108.6 111.88 Z" fill="rgba(0, 0, 0, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 203.53 250.15 L 203.5 188.1 Q 203.5 178.1 213.5 178.1 L 247.32 178.1" fill="none" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/><path d="M 252.57 178.1 L 245.57 181.6 L 247.32 178.1 L 245.57 174.6 Z" fill="rgba(0, 0, 0, 1)" stroke="rgba(0, 0, 0, 1)" stroke-miterlimit="10" pointer-events="none"/></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg>
\ No newline at end of file
diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/processors/rest/JettyRestProcessorAbstractSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/processors/rest/JettyRestProcessorAbstractSelfTest.java
index 8ca11b7..3e7398f 100644
--- a/modules/clients/src/test/java/org/apache/ignite/internal/processors/rest/JettyRestProcessorAbstractSelfTest.java
+++ b/modules/clients/src/test/java/org/apache/ignite/internal/processors/rest/JettyRestProcessorAbstractSelfTest.java
@@ -140,8 +140,11 @@
 import org.apache.ignite.lang.IgniteUuid;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import static org.apache.ignite.IgniteSystemProperties.IGNITE_MARSHALLER_BLACKLIST;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
 import static org.apache.ignite.cache.CacheMode.PARTITIONED;
 import static org.apache.ignite.cache.CacheMode.REPLICATED;
 import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_ASYNC;
@@ -166,14 +169,26 @@
  * Tests for Jetty REST protocol.
  */
 @SuppressWarnings("unchecked")
+@RunWith(Parameterized.class)
 public abstract class JettyRestProcessorAbstractSelfTest extends JettyRestProcessorCommonSelfTest {
     /** */
     private static boolean memoryMetricsEnabled;
 
+    /** */
+    @Parameterized.Parameter
+    public boolean useBinaryArrays;
+
+    /** Generates values for the {@link #useBinaryArrays} parameter. */
+    @Parameterized.Parameters(name = "useBinaryArrays = {0}")
+    public static Iterable<Object[]> useBinaryArrays() {
+        return Arrays.asList(new Object[][] {{true}, {false}});
+    }
+
     /** {@inheritDoc} */
     @Override protected void beforeTestsStarted() throws Exception {
         String path = U.resolveIgnitePath("modules/core/src/test/config/class_list_exploit_included.txt").getPath();
         System.setProperty(IGNITE_MARSHALLER_BLACKLIST, path);
+        System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(useBinaryArrays));
 
         super.beforeTestsStarted();
 
@@ -183,6 +198,7 @@
     /** {@inheritDoc} */
     @Override protected void afterTestsStopped() throws Exception {
         System.clearProperty(IGNITE_MARSHALLER_BLACKLIST);
+        System.clearProperty(IGNITE_USE_BINARY_ARRAYS);
 
         super.afterTestsStopped();
     }
@@ -1843,32 +1859,37 @@
 
         IgniteCacheProxy<Integer, String> c = (IgniteCacheProxy<Integer, String>)grid(1).createCache(partialCacheCfg);
 
-        Collection<GridCacheSqlMetadata> metas = c.context().queries().sqlMetadata();
+        try {
+            Collection<GridCacheSqlMetadata> metas = c.context().queries().sqlMetadata();
 
-        String ret = content("", GridRestCommand.CACHE_METADATA);
+            String ret = content("", GridRestCommand.CACHE_METADATA);
 
-        info("Cache metadata: " + ret);
+            info("Cache metadata: " + ret);
 
-        JsonNode arrRes = validateJsonResponse(ret);
+            JsonNode arrRes = validateJsonResponse(ret);
 
-        // TODO: IGNITE-7740 uncomment after IGNITE-7740 will be fixed.
-        // int cachesCnt = grid(1).cacheNames().size();
-        // assertEquals(cachesCnt, arrRes.size());
+            // TODO: IGNITE-7740 uncomment after IGNITE-7740 will be fixed.
+            // int cachesCnt = grid(1).cacheNames().size();
+            // assertEquals(cachesCnt, arrRes.size());
 
-        testMetadata(metas, arrRes);
+            testMetadata(metas, arrRes);
 
-        ret = content("person", GridRestCommand.CACHE_METADATA);
+            ret = content("person", GridRestCommand.CACHE_METADATA);
 
-        info("Cache metadata with cacheName parameter: " + ret);
+            info("Cache metadata with cacheName parameter: " + ret);
 
-        arrRes = validateJsonResponse(ret);
+            arrRes = validateJsonResponse(ret);
 
-        assertEquals(1, arrRes.size());
+            assertEquals(1, arrRes.size());
 
-        testMetadata(metas, arrRes);
+            testMetadata(metas, arrRes);
 
-        assertResponseContainsError(content("nonExistingCacheName", GridRestCommand.CACHE_METADATA),
-            "Failed to request meta data. nonExistingCacheName is not found");
+            assertResponseContainsError(content("nonExistingCacheName", GridRestCommand.CACHE_METADATA),
+                "Failed to request meta data. nonExistingCacheName is not found");
+        }
+        finally {
+            grid(1).destroyCache("partial");
+        }
     }
 
     /**
diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
index cea2d07..43ec92f 100644
--- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
+++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
@@ -692,6 +692,7 @@
                 "SYS.CACHES.REBALANCE_ORDER.null.10",
                 "SYS.CACHES.EVICTION_FILTER.null.2147483647",
                 "SYS.CACHES.EVICTION_POLICY_FACTORY.null.2147483647",
+                "SYS.CACHES.CONFLICT_RESOLVER.null.2147483647",
                 "SYS.CACHES.IS_NEAR_CACHE_ENABLED.null.1",
                 "SYS.CACHES.NEAR_CACHE_EVICTION_POLICY_FACTORY.null.2147483647",
                 "SYS.CACHES.NEAR_CACHE_START_SIZE.null.10",
diff --git a/modules/clients/src/test/java/org/apache/ignite/qa/query/WarningOnBigQueryResultsBaseTest.java b/modules/clients/src/test/java/org/apache/ignite/qa/query/WarningOnBigQueryResultsBaseTest.java
index 70f06c4..e463cb6 100644
--- a/modules/clients/src/test/java/org/apache/ignite/qa/query/WarningOnBigQueryResultsBaseTest.java
+++ b/modules/clients/src/test/java/org/apache/ignite/qa/query/WarningOnBigQueryResultsBaseTest.java
@@ -33,6 +33,7 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.processors.cache.index.AbstractIndexingCommonTest;
+import org.apache.ignite.internal.util.GridLogThrottle;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.testframework.GridTestUtils;
@@ -63,8 +64,8 @@
 
     /** Log message pattern. */
     private static final Pattern logPtrn = Pattern.compile(
-        "fetched=([0-9]+), duration=([0-9]+)ms, type=(MAP|LOCAL|REDUCE), distributedJoin=(true|false), enforceJoinOrder=(true|false), " +
-            "lazy=(true|false), schema=(\\S+), sql");
+        "fetched=([0-9]+), duration=([0-9]+)ms, type=(MAP|LOCAL|REDUCE), distributedJoin=(true|false), " +
+            "enforceJoinOrder=(true|false), lazy=(true|false), schema=(\\S+), sql");
 
     /** Test log. */
     private static Map<String, BigResultsLogListener> logListeners = new HashMap<>();
@@ -124,6 +125,9 @@
     @Override protected void beforeTestsStarted() throws Exception {
         super.beforeTestsStarted();
 
+        // Negative timeout to disable a throttling of the huge results warning messages.
+        GridLogThrottle.throttleTimeout(-1);
+
         // Starts the first node.
         startGrid(0);
 
@@ -162,6 +166,8 @@
     @Override protected void afterTestsStopped() throws Exception {
         stopAllGrids();
 
+        GridLogThrottle.throttleTimeout(GridLogThrottle.DFLT_THROTTLE_TIMEOUT);
+
         super.afterTestsStopped();
     }
 
@@ -269,10 +275,7 @@
                 schema = m.group(7);
 
                 sql = s.substring(s.indexOf(", sql='") + 7, s.indexOf("', plan="));
-                if ("REDUCE".equals(type))
-                    plan = s.substring(s.indexOf("', plan=") + 8, s.indexOf(", reqId="));
-                else
-                    plan = s.substring(s.indexOf("', plan=") + 8, s.indexOf(", node="));
+                plan = s.substring(s.indexOf("', plan=") + 8, s.indexOf(", reqId="));
 
                 assertTrue(sql.contains("SELECT"));
                 assertTrue(plan.contains("SELECT"));
diff --git a/modules/codegen/src/main/java/org/apache/ignite/codegen/SystemViewRowAttributeWalkerGenerator.java b/modules/codegen/src/main/java/org/apache/ignite/codegen/SystemViewRowAttributeWalkerGenerator.java
index 6beb0fa..919a9dc 100644
--- a/modules/codegen/src/main/java/org/apache/ignite/codegen/SystemViewRowAttributeWalkerGenerator.java
+++ b/modules/codegen/src/main/java/org/apache/ignite/codegen/SystemViewRowAttributeWalkerGenerator.java
@@ -58,6 +58,7 @@
 import org.apache.ignite.spi.systemview.view.PartitionStateView;
 import org.apache.ignite.spi.systemview.view.ScanQueryView;
 import org.apache.ignite.spi.systemview.view.ServiceView;
+import org.apache.ignite.spi.systemview.view.SnapshotView;
 import org.apache.ignite.spi.systemview.view.SqlIndexView;
 import org.apache.ignite.spi.systemview.view.SqlQueryHistoryView;
 import org.apache.ignite.spi.systemview.view.SqlQueryView;
@@ -140,6 +141,7 @@
         gen.generateAndWrite(NodeAttributeView.class, DFLT_SRC_DIR);
         gen.generateAndWrite(NodeMetricsView.class, DFLT_SRC_DIR);
         gen.generateAndWrite(CacheGroupIoView.class, DFLT_SRC_DIR);
+        gen.generateAndWrite(SnapshotView.class, DFLT_SRC_DIR);
 
         gen.generateAndWrite(SqlSchemaView.class, INDEXING_SRC_DIR);
         gen.generateAndWrite(SqlTableView.class, INDEXING_SRC_DIR);
diff --git a/modules/compatibility/pom.xml b/modules/compatibility/pom.xml
index 5df6a92..b7c26b8 100644
--- a/modules/compatibility/pom.xml
+++ b/modules/compatibility/pom.xml
@@ -61,6 +61,12 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-model</artifactId>
+            <version>${maven.model.version}</version>
+        </dependency>
+
+        <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>ignite-core</artifactId>
             <version>${project.version}</version>
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/IgniteReleasedVersion.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/IgniteReleasedVersion.java
new file mode 100644
index 0000000..8920327
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/IgniteReleasedVersion.java
@@ -0,0 +1,98 @@
+/*
+ * 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.compatibility;
+
+import java.util.Collection;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.lang.IgniteProductVersion;
+
+/** Released Ignite versions. */
+@SuppressWarnings("unused")
+public enum IgniteReleasedVersion {
+    /** */
+    VER_2_1_0("2.1.0"),
+
+    /** */
+    VER_2_2_0("2.2.0"),
+
+    /** */
+    VER_2_3_0("2.3.0"),
+
+    /** */
+    VER_2_4_0("2.4.0"),
+
+    /** */
+    VER_2_5_0("2.5.0"),
+
+    /** */
+    VER_2_6_0("2.6.0"),
+
+    /** */
+    VER_2_7_0("2.7.0"),
+
+    /** */
+    VER_2_7_6("2.7.6"),
+
+    /** */
+    VER_2_8_0("2.8.0"),
+
+    /** */
+    VER_2_8_1("2.8.1"),
+
+    /** */
+    VER_2_9_0("2.9.0"),
+
+    /** */
+    VER_2_9_1("2.9.1"),
+
+    /** */
+    VER_2_10_0("2.10.0"),
+
+    /** */
+    VER_2_11_0("2.11.0"),
+
+    /** */
+    VER_2_12_0("2.12.0");
+
+    /** Ignite version. */
+    private final IgniteProductVersion ver;
+
+    /** @param ver Ignite version. */
+    IgniteReleasedVersion(String ver) {
+        this.ver = IgniteProductVersion.fromString(ver);
+    }
+
+    /** @return Ignite version. */
+    public IgniteProductVersion version() {
+        return ver;
+    }
+
+    /**
+     * @return Ignite versions since provided version.
+     * @param ver Version.
+     */
+    public static Collection<String> since(IgniteReleasedVersion ver) {
+        return F.transform(F.view(F.asList(values()), v -> v.version().compareTo(ver.version()) >= 0),
+            IgniteReleasedVersion::toString);
+    }
+
+    /** @return String representation of three-part version number. */
+    @Override public String toString() {
+        return ver.major() + "." + ver.minor() + "." + ver.maintenance();
+    }
+}
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/AbstractClientCompatibilityTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/AbstractClientCompatibilityTest.java
index c12cf1c..e6cb495 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/AbstractClientCompatibilityTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/AbstractClientCompatibilityTest.java
@@ -17,9 +17,7 @@
 
 package org.apache.ignite.compatibility.clients;
 
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.stream.Collectors;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.compatibility.testframework.junits.Dependency;
 import org.apache.ignite.compatibility.testframework.junits.IgniteCompatibilityAbstractTest;
@@ -27,6 +25,7 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteVersionUtils;
 import org.apache.ignite.internal.util.GridJavaProcess;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteInClosure;
@@ -39,6 +38,10 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.VER_2_4_0;
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.since;
+import static org.apache.ignite.testframework.GridTestUtils.cartesianProduct;
+
 /**
  * Tests that current client version can connect to the server with specified version and
  * specified client version can connect to the current server version.
@@ -63,28 +66,16 @@
     /** Version 2.11.0. */
     protected static final IgniteProductVersion VER_2_11_0 = IgniteProductVersion.fromString("2.11.0");
 
-    /** Ignite versions to test. Note: Only released versions or current version should be included to this list. */
-    protected static final String[] TESTED_IGNITE_VERSIONS = new String[] {
-        "2.4.0",
-        "2.5.0",
-        "2.6.0",
-        "2.7.0",
-        "2.7.5",
-        "2.7.6",
-        "2.8.0",
-        "2.8.1",
-        "2.9.0",
-        "2.9.1",
-        "2.10.0",
-        IgniteVersionUtils.VER_STR
-    };
+    /** Version 2.12.0. */
+    protected static final IgniteProductVersion VER_2_12_0 = IgniteProductVersion.fromString("2.12.0");
+
+    /** Version 2.13.0. */
+    protected static final IgniteProductVersion VER_2_13_0 = IgniteProductVersion.fromString("2.13.0");
 
     /** Parameters. */
     @Parameterized.Parameters(name = "Version {0}")
     public static Iterable<Object[]> versions() {
-        return Arrays.stream(TESTED_IGNITE_VERSIONS)
-            .map(v -> new Object[] {v})
-            .collect(Collectors.toList());
+        return cartesianProduct(F.concat(true, IgniteVersionUtils.VER_STR, since(VER_2_4_0)));
     }
 
     /** Old Ignite version. */
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/JavaThinCompatibilityTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/JavaThinCompatibilityTest.java
index 3aa6544..b9f85f1 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/JavaThinCompatibilityTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/clients/JavaThinCompatibilityTest.java
@@ -38,6 +38,8 @@
 import org.apache.ignite.cache.query.ScanQuery;
 import org.apache.ignite.client.ClientCache;
 import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.ClientFeatureNotSupportedByServerException;
+import org.apache.ignite.client.ClientServiceDescriptor;
 import org.apache.ignite.client.ClientTransaction;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.client.Person;
@@ -53,7 +55,10 @@
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.platform.PlatformType;
+import org.apache.ignite.resources.ServiceContextResource;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallContext;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.jetbrains.annotations.NotNull;
@@ -62,6 +67,10 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.internal.client.thin.ProtocolBitmaskFeature.GET_SERVICE_DESCRIPTORS;
+import static org.apache.ignite.internal.client.thin.ProtocolBitmaskFeature.SERVICE_INVOKE_CALLCTX;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
+
 /**
  * Tests java thin client compatibility. This test only checks that thin client can perform basic operations with
  * different client and server versions. Whole API not checked, corner cases not checked.
@@ -84,6 +93,9 @@
     @Override protected void initNode(Ignite ignite) {
         ignite.services().deployNodeSingleton("test_service", new EchoService());
 
+        if (ver.compareTo(VER_2_13_0) >= 0)
+            ignite.services().deployNodeSingleton("ctx_service", new CtxService());
+
         super.initNode(ignite);
     }
 
@@ -318,6 +330,34 @@
     }
 
     /** */
+    private void testServicesWithCallerContext() {
+        X.println(">>>> Testing services with caller context");
+
+        ServiceCallContext callCtx = ServiceCallContext.builder().put("key", "value").build();
+
+        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(ADDR))) {
+            assertEquals("value", client.services().serviceProxy("ctx_service", CtxServiceInterface.class, callCtx)
+                .attribute("key"));
+        }
+    }
+
+    /** */
+    private void testServicesWithCallerContextThrows() {
+        X.println(">>>> Testing services with caller context throws");
+
+        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(ADDR))) {
+            ServiceCallContext callCtx = ServiceCallContext.builder().put("key", "value").build();
+
+            EchoServiceInterface svc =
+                client.services().serviceProxy("test_service", EchoServiceInterface.class, callCtx);
+
+            Throwable err = assertThrowsWithCause(() -> svc.echo(1), ClientFeatureNotSupportedByServerException.class);
+
+            assertEquals("Feature " + SERVICE_INVOKE_CALLCTX.name() + " is not supported by the server", err.getMessage());
+        }
+    }
+
+    /** */
     private void testContinuousQueries() throws Exception {
         X.println(">>>> Testing continuous queries");
 
@@ -371,6 +411,59 @@
 
         if (clientVer.compareTo(VER_2_11_0) >= 0 && serverVer.compareTo(VER_2_10_0) >= 0)
             testContinuousQueries();
+
+        if (clientVer.compareTo(VER_2_13_0) >= 0) {
+            if (serverVer.compareTo(VER_2_13_0) >= 0) {
+                testServiceDescriptors();
+                testServicesWithCallerContext();
+            }
+            else {
+                testServiceDescriptorsThrows();
+                testServicesWithCallerContextThrows();
+            }
+        }
+    }
+
+    /** */
+    private void testServiceDescriptors() {
+        X.println(">>>> Testing services descriptors");
+
+        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(ADDR))) {
+            assertEquals(2, client.services().serviceDescriptors().size());
+
+            ClientServiceDescriptor svc = client.services().serviceDescriptor("test_service");
+
+            assertEquals("test_service", svc.name());
+            assertEquals(EchoService.class.getName(), svc.serviceClass());
+            assertEquals(0, svc.totalCount());
+            assertEquals(1, svc.maxPerNodeCount());
+            assertNull(svc.cacheName());
+            assertEquals(grid(0).localNode().id(), svc.originNodeId());
+            assertEquals(PlatformType.JAVA, svc.platformType());
+        }
+    }
+
+    /** */
+    private void testServiceDescriptorsThrows() {
+        X.println(">>>> Testing services descriptors queries throws");
+
+        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(ADDR))) {
+            String errMsg = "Feature " + GET_SERVICE_DESCRIPTORS.name() + " is not supported by the server";
+
+            Throwable err = assertThrowsWithCause(
+                () -> client.services().serviceDescriptors(),
+                ClientFeatureNotSupportedByServerException.class
+            );
+
+            assertEquals(errMsg, err.getMessage());
+
+            err = assertThrowsWithCause(
+                () -> client.services().serviceDescriptor("test_service"),
+                ClientFeatureNotSupportedByServerException.class
+            );
+
+            assertEquals(errMsg, err.getMessage());
+        }
     }
 
     /** */
@@ -402,6 +495,26 @@
         }
     }
 
+    /** Service for testing the attributes of a service call context. */
+    public static interface CtxServiceInterface {
+        /** */
+        public String attribute(String name);
+    }
+
+    /** */
+    public static class CtxService implements Service, CtxServiceInterface {
+        /** Service context. */
+        @ServiceContextResource
+        private ServiceContext svcCtx;
+
+        /** {@inheritDoc} */
+        @Override public String attribute(String name) {
+            ServiceCallContext callCtx = svcCtx.currentCallContext();
+
+            return svcCtx == null ? null : callCtx.attribute(name);
+        }
+    }
+
     /** */
     public static class EchoJob implements ComputeJob {
         /** Value. */
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IgnitePKIndexesMigrationToUnwrapPkTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IgnitePKIndexesMigrationToUnwrapPkTest.java
index d0504ef..0e15720 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IgnitePKIndexesMigrationToUnwrapPkTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IgnitePKIndexesMigrationToUnwrapPkTest.java
@@ -27,6 +27,7 @@
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.SystemDataRegionConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.processors.cache.GridCacheAbstractFullApiSelfTest;
 import org.apache.ignite.lang.IgniteInClosure;
@@ -248,14 +249,20 @@
 
             cfg.setPeerClassLoadingEnabled(false);
 
-            DataStorageConfiguration memCfg = new DataStorageConfiguration()
+            DataStorageConfiguration storageCfg = new DataStorageConfiguration()
                 .setDefaultDataRegionConfiguration(
-                    new DataRegionConfiguration().setPersistenceEnabled(true)
-                        .setInitialSize(1024 * 1024 * 10).setMaxSize(1024 * 1024 * 15))
-                .setSystemRegionInitialSize(1024 * 1024 * 10)
-                .setSystemRegionMaxSize(1024 * 1024 * 15);
+                    new DataRegionConfiguration()
+                            .setPersistenceEnabled(true)
+                            .setInitialSize(10 * 1024 * 1024)
+                            .setMaxSize(15 * 1024 * 1024)
+                )
+                .setSystemDataRegionConfiguration(
+                    new SystemDataRegionConfiguration()
+                            .setInitialSize(10 * 1024 * 1024)
+                            .setMaxSize(15 * 1024 * 1024)
+                );
 
-            cfg.setDataStorageConfiguration(memCfg);
+            cfg.setDataStorageConfiguration(storageCfg);
         }
     }
 }
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IndexTypesCompatibilityTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IndexTypesCompatibilityTest.java
index 7071d61..b855c22 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IndexTypesCompatibilityTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/IndexTypesCompatibilityTest.java
@@ -43,6 +43,10 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.VER_2_6_0;
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.since;
+import static org.apache.ignite.testframework.GridTestUtils.cartesianProduct;
+
 /**
  * Checks all basic sql types work correctly.
  */
@@ -96,12 +100,10 @@
     /** Test run configurations: Ignite version, Inline size configuration. */
     @Parameterized.Parameters(name = "ver={0}")
     public static Collection<Object[]> runConfig() {
-        return Arrays.asList(new Object[][] {
-            {"2.6.0"}, {"2.7.0"}, {"2.7.6"}, {"2.8.0"}, {"2.8.1"}, {"2.9.0"}, {"2.9.1"}, {"2.10.0"}
-        });
+        return cartesianProduct(since(VER_2_6_0));
     }
 
-        /** */
+    /** */
     @Test
     public void testQueryOldIndex() throws Exception {
         doTestStartupWithOldVersion(igniteVer, new PostStartupClosure());
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/InlineJavaObjectCompatibilityTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/InlineJavaObjectCompatibilityTest.java
index a81633b..1becef2 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/InlineJavaObjectCompatibilityTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/InlineJavaObjectCompatibilityTest.java
@@ -21,7 +21,6 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Random;
@@ -33,11 +32,15 @@
 import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.lang.IgniteInClosure;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.VER_2_6_0;
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.since;
+import static org.apache.ignite.testframework.GridTestUtils.cartesianProduct;
 
 /**
  * Tests that upgrade version on persisted inline index is successfull.
@@ -67,32 +70,8 @@
     /** Test run configurations: Ignite version, Inline size configuration. */
     @Parameterized.Parameters(name = "ver={0}, cfgInlineSize={1}")
     public static Collection<Object[]> runConfig() {
-        return Arrays.asList(new Object[][] {
-            /** 2.6.0 is a last version where POJO inlining isn't enabled. */
-            {"2.6.0", false},
-            {"2.6.0", true},
-
-            {"2.7.0", false},
-            {"2.7.0", true},
-
-            {"2.7.6", false},
-            {"2.7.6", true},
-
-            {"2.8.0", false},
-            {"2.8.0", true},
-
-            {"2.8.1", false},
-            {"2.8.1", true},
-
-            {"2.9.0", false},
-            {"2.9.0", true},
-
-            {"2.9.1", false},
-            {"2.9.1", true},
-
-            {"2.10.0", false},
-            {"2.10.0", true}
-        });
+        /** 2.6.0 is a last version where POJO inlining isn't enabled. */
+        return cartesianProduct(since(VER_2_6_0), F.asList(false, true));
     }
 
     /** */
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/PersistenceBasicCompatibilityTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/PersistenceBasicCompatibilityTest.java
index f11643e..f14580f 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/PersistenceBasicCompatibilityTest.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/PersistenceBasicCompatibilityTest.java
@@ -22,6 +22,7 @@
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.UUID;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
@@ -39,10 +40,17 @@
 import org.apache.ignite.lang.IgniteInClosure;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.VER_2_1_0;
+import static org.apache.ignite.compatibility.IgniteReleasedVersion.since;
+import static org.apache.ignite.testframework.GridTestUtils.cartesianProduct;
 
 /**
  * Saves data using previous version of ignite and then load this data using actual version.
  */
+@RunWith(Parameterized.class)
 public class PersistenceBasicCompatibilityTest extends IgnitePersistenceCompatibilityAbstractTest {
     /** */
     protected static final String TEST_CACHE_NAME = PersistenceBasicCompatibilityTest.class.getSimpleName();
@@ -50,6 +58,16 @@
     /** */
     protected volatile boolean compactFooter;
 
+    /** Old Ignite version. */
+    @Parameterized.Parameter
+    public String version;
+
+    /** Parameters. */
+    @Parameterized.Parameters(name = "version={0}")
+    public static Collection<Object[]> parameters() {
+        return cartesianProduct(since(VER_2_1_0));
+    }
+
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
@@ -78,126 +96,8 @@
      * @throws Exception If failed.
      */
     @Test
-    public void testNodeStartByOldVersionPersistenceData_2_1() throws Exception {
-        doTestStartupWithOldVersion("2.1.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_2() throws Exception {
-        doTestStartupWithOldVersion("2.2.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_3() throws Exception {
-        doTestStartupWithOldVersion("2.3.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_4() throws Exception {
-        doTestStartupWithOldVersion("2.4.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_5() throws Exception {
-        doTestStartupWithOldVersion("2.5.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_6() throws Exception {
-        doTestStartupWithOldVersion("2.6.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_7() throws Exception {
-        doTestStartupWithOldVersion("2.7.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_7_6() throws Exception {
-        doTestStartupWithOldVersion("2.7.6");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_8() throws Exception {
-        doTestStartupWithOldVersion("2.8.0");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_8_1() throws Exception {
-        doTestStartupWithOldVersion("2.8.1");
-    }
-
-    /**
-     * Tests opportunity to read data from previous Ignite DB version.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_9() throws Exception {
-        doTestStartupWithOldVersion("2.9.0");
-    }
-
-    /** @throws Exception If failed. */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_9_1() throws Exception {
-        doTestStartupWithOldVersion("2.9.1");
-    }
-
-    /** @throws Exception If failed. */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_10() throws Exception {
-        doTestStartupWithOldVersion("2.10.0");
-    }
-
-    /** @throws Exception If failed. */
-    @Test
-    public void testNodeStartByOldVersionPersistenceData_2_11() throws Exception {
-        doTestStartupWithOldVersion("2.11.0");
+    public void testNodeStartByOldVersionPersistenceData() throws Exception {
+        doTestStartupWithOldVersion(version);
     }
 
     /**
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/util/MavenUtils.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/util/MavenUtils.java
index 81331ab..a3d1b4f 100644
--- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/util/MavenUtils.java
+++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/util/MavenUtils.java
@@ -18,23 +18,33 @@
 package org.apache.ignite.compatibility.testframework.util;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import com.google.common.base.Charsets;
 import com.google.common.io.CharStreams;
+import org.apache.ignite.internal.util.GridStringBuilder;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.SB;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.RepositoryBase;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static org.apache.ignite.internal.util.lang.GridFunc.isEmpty;
+
 /**
  * Provides some useful methods to work with Maven.
  */
@@ -43,6 +53,9 @@
     private static String locRepPath = null;
 
     /** */
+    private static final String MAVEN_DEPENDENCY_PLUGIN = "org.apache.maven.plugins:maven-dependency-plugin:3.2.0";
+
+    /** */
     private static final String GG_MVN_REPO = "http://www.gridgainsystems.com/nexus/content/repositories/external";
 
     /** Set this flag to true if running PDS compatibility tests locally. */
@@ -142,6 +155,29 @@
         return output.substring(output.lastIndexOf('>', endTagPos) + 1, endTagPos);
     }
 
+    /** @return Collection of configured repositories for the Maven project. */
+    private static Collection<String> mavenProjectRepositories() throws Exception {
+        String workDir = System.getProperty("user.dir");
+
+        File prjPomFile = new File(workDir, "pom.xml");
+
+        if (!prjPomFile.exists())
+            return Collections.emptyList();
+
+        Path outPath = Files.createTempFile("effective-pom", "");
+
+        try {
+            exec(buildMvnCommand() + " -f " + workDir + " help:effective-pom -Doutput=" + outPath.toAbsolutePath());
+
+            Model model = new MavenXpp3Reader().read(new FileInputStream(outPath.toFile()));
+
+            return F.transform(model.getRepositories(), RepositoryBase::getUrl);
+        }
+        finally {
+            Files.deleteIfExists(outPath);
+        }
+    }
+
     /**
      * Downloads and stores in local repository an artifact with given identifier.
      *
@@ -156,15 +192,24 @@
 
         String localProxyMavenSettingsFromEnv = System.getenv("LOCAL_PROXY_MAVEN_SETTINGS");
 
-        SB mavenCommandArgs = new SB(" org.apache.maven.plugins:maven-dependency-plugin:3.0.2:get -Dartifact=" + artifact);
+        GridStringBuilder mavenCommandArgs = new SB(" ").a(MAVEN_DEPENDENCY_PLUGIN).a(":get -Dartifact=" + artifact);
 
-        if (!F.isEmpty(localProxyMavenSettingsFromEnv))
+        if (!isEmpty(localProxyMavenSettingsFromEnv))
             localProxyMavenSettings = Paths.get(localProxyMavenSettingsFromEnv);
 
         if (Files.exists(localProxyMavenSettings))
             mavenCommandArgs.a(" -s " + localProxyMavenSettings.toString());
-        else
-            mavenCommandArgs.a(useGgRepo ? " -DremoteRepositories=" + GG_MVN_REPO : "");
+        else {
+            Collection<String> repos = new ArrayList<>();
+
+            if (useGgRepo)
+                repos.add(GG_MVN_REPO);
+
+            repos.addAll(mavenProjectRepositories());
+
+            if (!repos.isEmpty())
+                mavenCommandArgs.a(" -DremoteRepositories=").a(String.join(",", repos));
+        }
 
         exec(buildMvnCommand() + mavenCommandArgs.toString());
 
diff --git a/modules/control-utility/pom.xml b/modules/control-utility/pom.xml
index e0874d9..5a7f787 100644
--- a/modules/control-utility/pom.xml
+++ b/modules/control-utility/pom.xml
@@ -102,6 +102,13 @@
         </dependency>
 
         <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
             <scope>test</scope>
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/consistency/ConsistencyCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/consistency/ConsistencyCommand.java
index 46e2760..4982a98 100644
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/consistency/ConsistencyCommand.java
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/consistency/ConsistencyCommand.java
@@ -22,6 +22,7 @@
 import java.util.Map;
 import java.util.logging.Logger;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.client.GridClient;
 import org.apache.ignite.internal.client.GridClientConfiguration;
 import org.apache.ignite.internal.commandline.AbstractCommand;
@@ -42,6 +43,15 @@
  *
  */
 public class ConsistencyCommand extends AbstractCommand<Object> {
+    /** Cache. */
+    public static final String CACHE = "--cache";
+
+    /** Partition. */
+    public static final String PARTITION = "--partition";
+
+    /** Strategy. */
+    public static final String STRATEGY = "--strategy";
+
     /** Command argument. */
     private VisorConsistencyRepairTaskArg cmdArg;
 
@@ -131,10 +141,50 @@
         cmd = of(argIter.nextArg("Expected consistency action."));
 
         if (cmd == REPAIR) {
-            String cacheName = argIter.nextArg("Expected cache name.");
-            int part = argIter.nextNonNegativeIntArg("Expected partition.");
+            String cacheName = null;
+            int part = -1;
+            ReadRepairStrategy strategy = null;
 
-            cmdArg = new VisorConsistencyRepairTaskArg(cacheName, part);
+            while (argIter.hasNextArg()) {
+                String arg = argIter.peekNextArg();
+
+                if (CACHE.equals(arg) || PARTITION.equals(arg) || STRATEGY.equals(arg)) {
+                    arg = argIter.nextArg("Expected parameter key.");
+
+                    switch (arg) {
+                        case CACHE:
+                            cacheName = argIter.nextArg("Expected cache name.");
+
+                            break;
+
+                        case PARTITION:
+                            part = argIter.nextNonNegativeIntArg("Expected partition.");
+
+                            break;
+
+                        case STRATEGY:
+                            strategy = ReadRepairStrategy.fromString(argIter.nextArg("Expected strategy."));
+
+                            break;
+
+                        default:
+                            throw new IllegalArgumentException("Illegal argument: " + arg);
+                    }
+                }
+                else
+                    break;
+            }
+
+            if (cacheName == null)
+                throw new IllegalArgumentException("Cache name argument missed.");
+
+            if (part == -1)
+                throw new IllegalArgumentException("Partition argument missed.");
+
+            if (strategy == null)
+                throw new IllegalArgumentException("Strategy argument missed.");
+
+            cmdArg = new VisorConsistencyRepairTaskArg(cacheName, part, strategy);
         }
         else if (cmd == STATUS)
             cmdArg = null;
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
index 22432a4..7b25b35 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
@@ -58,6 +58,7 @@
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.IgniteVersionUtils;
 import org.apache.ignite.internal.commandline.CommandHandler;
 import org.apache.ignite.internal.commandline.CommandList;
 import org.apache.ignite.internal.commandline.CommonArgParser;
@@ -112,6 +113,9 @@
 import static org.apache.ignite.internal.commandline.cache.CacheDestroy.DESTROY_ALL_ARG;
 import static org.apache.ignite.internal.commandline.cache.CacheSubcommands.DESTROY;
 import static org.apache.ignite.internal.commandline.cache.CacheSubcommands.HELP;
+import static org.apache.ignite.internal.commandline.consistency.ConsistencyCommand.CACHE;
+import static org.apache.ignite.internal.commandline.consistency.ConsistencyCommand.PARTITION;
+import static org.apache.ignite.internal.commandline.consistency.ConsistencyCommand.STRATEGY;
 import static org.apache.ignite.testframework.GridTestUtils.assertContains;
 import static org.apache.ignite.testframework.GridTestUtils.assertNotContains;
 import static org.apache.ignite.testframework.GridTestUtils.readResource;
@@ -131,6 +135,9 @@
     /** Special word for defining any char sequence from special word to the end of line in golden copy of help output */
     private static final String ANY = "<!any!>";
 
+    /** Special word for defining copyright message in golden copy of help output. */
+    private static final String COPYRIGHT = "<!copyright!>";
+
     /** Error stack trace prefix. */
     protected static final String ERROR_STACK_TRACE_PREFIX = "Error stack trace:";
 
@@ -395,6 +402,9 @@
 
             for (int i = 0; i < correctOutputLines.size(); i++) {
                 String cLine = correctOutputLines.get(i);
+
+                cLine = cLine.replace(COPYRIGHT, IgniteVersionUtils.COPYRIGHT);
+
                 // Remove all spaces from end of line.
                 String line = outputLines.get(i).replaceAll("\\s+$", "");
 
@@ -1654,7 +1664,8 @@
         cmdArgs.put(WAL, asList(new String[] {"print"}, new String[] {"delete"}));
         cmdArgs.put(METADATA, asList(new String[] {"help"}, new String[] {"list"}));
         cmdArgs.put(TRACING_CONFIGURATION, Collections.singletonList(new String[] {"get_all"}));
-        cmdArgs.put(CONSISTENCY, Collections.singletonList(new String[] {"repair", "cache", "0"}));
+        cmdArgs.put(CONSISTENCY, Collections.singletonList(
+            new String[] {"repair", CACHE, "cache", PARTITION, "0", STRATEGY, "LWW"}));
 
         String warning = String.format(
             "To use experimental command add --enable-experimental parameter for %s",
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerConsistencyTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerConsistencyTest.java
index 6002439..f3c8020 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerConsistencyTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerConsistencyTest.java
@@ -23,10 +23,12 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.commandline.consistency.ConsistencyCommand;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.CacheObjectImpl;
 import org.apache.ignite.internal.processors.cache.GridCacheAdapter;
@@ -37,10 +39,11 @@
 import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import static org.apache.ignite.cache.CacheAtomicityMode.ATOMIC;
 import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
-import static org.apache.ignite.events.EventType.EVT_CONSISTENCY_VIOLATION;
 import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK;
 import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_UNEXPECTED_ERROR;
 import static org.apache.ignite.internal.visor.consistency.VisorConsistencyRepairTask.CONSISTENCY_VIOLATIONS_FOUND;
@@ -49,6 +52,7 @@
 /**
  *
  */
+@RunWith(Parameterized.class)
 public class GridCommandHandlerConsistencyTest extends GridCommandHandlerClusterPerMethodAbstractTest {
     /** Default cache name atomic. */
     private static final String DEFAULT_CACHE_NAME_ATOMIC = DEFAULT_CACHE_NAME + "Atomic";
@@ -62,6 +66,23 @@
     /** Partitions. */
     private static final int PARTITIONS = 32;
 
+    /** */
+    @Parameterized.Parameters(name = "strategy={0}")
+    public static Iterable<Object[]> data() {
+        List<Object[]> res = new ArrayList<>();
+
+        for (ReadRepairStrategy strategy : ReadRepairStrategy.values())
+            res.add(new Object[] {strategy});
+
+        return res;
+    }
+
+    /**
+     *
+     */
+    @Parameterized.Parameter
+    public ReadRepairStrategy strategy;
+
     /**
      *
      */
@@ -81,7 +102,6 @@
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
 
         cfg.setDataStorageConfiguration(null);
-        cfg.setIncludeEventTypes(EVT_CONSISTENCY_VIOLATION);
 
         return cfg;
     }
@@ -122,13 +142,17 @@
         assertContains(log, testOut.toString(),
             "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts.get());
 
-        readRepairTx(brokenParts, txCacheName);
+        Integer fixesPerEntry = fixesPerEntry();
 
-        assertEquals(PARTITIONS, brokenParts.get()); // Half fixed.
+        readRepair(brokenParts, txCacheName, fixesPerEntry);
 
-        readRepaitAtomic(brokenParts, atomicCacheName);
+        if (fixesPerEntry != null && fixesPerEntry > 0)
+            assertEquals(PARTITIONS, brokenParts.get()); // Half fixed.
 
-        assertEquals(PARTITIONS, brokenParts.get()); // Atomics still broken.
+        readRepair(brokenParts, atomicCacheName, fixesPerEntry != null ? 0 : null);
+
+        if (fixesPerEntry != null && fixesPerEntry > 0)
+            assertEquals(PARTITIONS, brokenParts.get()); // Atomics still broken.
     }
 
     /**
@@ -164,9 +188,12 @@
         assertContains(log, testOut.toString(),
             "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts.get());
 
-        readRepairTx(brokenParts, cacheName);
+        Integer fixesPerEntry = fixesPerEntry();
 
-        assertEquals(0, brokenParts.get());
+        readRepair(brokenParts, cacheName, fixesPerEntry);
+
+        if (fixesPerEntry != null)
+            assertEquals(fixesPerEntry > 0 ? 0 : PARTITIONS, brokenParts.get());
     }
 
     /**
@@ -179,7 +206,12 @@
         injectTestSystemOut();
 
         for (int i = 0; i < PARTITIONS; i++) {
-            assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--consistency", "repair", "non-existent", String.valueOf(i)));
+            assertEquals(EXIT_CODE_UNEXPECTED_ERROR,
+                execute("--consistency", "repair",
+                    ConsistencyCommand.CACHE, "non-existent",
+                    ConsistencyCommand.PARTITION, String.valueOf(i),
+                    ConsistencyCommand.STRATEGY, strategy.toString()));
+
             assertContains(log, testOut.toString(), "Cache not found");
         }
     }
@@ -187,36 +219,51 @@
     /**
      *
      */
-    private void readRepairTx(AtomicInteger brokenParts, String cacheName) {
+    private void readRepair(AtomicInteger brokenParts, String cacheName, Integer fixesPerEntry) {
         for (int i = 0; i < PARTITIONS; i++) {
-            assertEquals(EXIT_CODE_OK, execute("--consistency", "repair", cacheName, String.valueOf(i)));
+            assertEquals(EXIT_CODE_OK, execute("--consistency", "repair",
+                ConsistencyCommand.CACHE, cacheName,
+                ConsistencyCommand.PARTITION, String.valueOf(i),
+                ConsistencyCommand.STRATEGY, strategy.toString()));
             assertContains(log, testOut.toString(), CONSISTENCY_VIOLATIONS_FOUND);
-            assertContains(log, testOut.toString(), "[found=1, fixed=1");
+            assertContains(log, testOut.toString(), "[found=1, fixed=" + (fixesPerEntry != null ? fixesPerEntry.toString() : ""));
 
             assertEquals(EXIT_CODE_OK, execute("--cache", "idle_verify"));
 
-            brokenParts.decrementAndGet();
+            if (fixesPerEntry != null)
+                if (fixesPerEntry > 0) {
+                    brokenParts.decrementAndGet();
 
-            if (brokenParts.get() > 0)
-                assertContains(log, testOut.toString(),
-                    "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts);
-            else
-                assertContains(log, testOut.toString(), "no conflicts have been found");
+                    if (brokenParts.get() > 0)
+                        assertContains(log, testOut.toString(),
+                            "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts);
+                    else
+                        assertContains(log, testOut.toString(), "no conflicts have been found");
+                }
+                else
+                    assertContains(log, testOut.toString(),
+                        "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts); // Nothing fixed.
         }
     }
 
     /**
      *
      */
-    private void readRepaitAtomic(AtomicInteger brokenParts, String cacheName) {
-        for (int i = 0; i < PARTITIONS; i++) { // This may be a copy of previous (tx case), implement atomic repair to make this happen :)
-            assertEquals(EXIT_CODE_OK, execute("--consistency", "repair", cacheName, String.valueOf(i)));
-            assertContains(log, testOut.toString(), CONSISTENCY_VIOLATIONS_FOUND);
-            assertContains(log, testOut.toString(), "[found=1, fixed=0"); // Nothing fixed.
+    private Integer fixesPerEntry() {
+        switch (strategy) {
+            case PRIMARY:
+            case REMOVE:
+                return 1;
 
-            assertEquals(EXIT_CODE_OK, execute("--cache", "idle_verify"));
-            assertContains(log, testOut.toString(),
-                "conflict partitions has been found: [counterConflicts=0, hashConflicts=" + brokenParts); // Nothing fixed.
+            case CHECK_ONLY:
+                return 0;
+
+            case RELATIVE_MAJORITY:
+            case LWW:
+                return null; // Who knows :)
+
+            default:
+                throw new UnsupportedOperationException("Unsupported strategy");
         }
     }
 
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexingTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexingTest.java
index 99217d7..147dff3 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexingTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexingTest.java
@@ -244,19 +244,9 @@
 
         forceCheckpoint();
 
-        File idxPath = indexPartition(ignite, GROUP_NAME);
+        enableCheckpoints(ignite, false);
 
-        stopAllGrids();
-
-        corruptIndexPartition(idxPath, 1024, 4096);
-
-        startGrids(GRID_CNT);
-
-        awaitPartitionMapExchange();
-
-        forceCheckpoint();
-
-        enableCheckpoints(G.allGrids(), false);
+        corruptIndexPartition(indexPartition(ignite, GROUP_NAME), 1024, 4096);
 
         injectTestSystemOut();
 
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
index 1241624..12a142d 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
@@ -23,6 +23,7 @@
 import java.io.Serializable;
 import java.lang.reflect.Field;
 import java.nio.file.Files;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -43,7 +44,6 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.LongAdder;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
@@ -71,6 +71,7 @@
 import org.apache.ignite.internal.GridJobExecuteResponse;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
 import org.apache.ignite.internal.IgniteNodeAttributes;
 import org.apache.ignite.internal.TestRecordingCommunicationSpi;
 import org.apache.ignite.internal.client.GridClientFactory;
@@ -91,6 +92,9 @@
 import org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager;
 import org.apache.ignite.internal.processors.cache.persistence.db.IgniteCacheGroupsWithRestartsTest;
 import org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.dumpprocessors.ToFileDumpProcessor;
+import org.apache.ignite.internal.processors.cache.persistence.file.FileIO;
+import org.apache.ignite.internal.processors.cache.persistence.file.FileIOFactory;
+import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager;
 import org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx;
 import org.apache.ignite.internal.processors.cache.transactions.IgniteTxEntry;
 import org.apache.ignite.internal.processors.cache.transactions.TransactionProxyImpl;
@@ -100,7 +104,6 @@
 import org.apache.ignite.internal.processors.cache.warmup.WarmUpTestPluginProvider;
 import org.apache.ignite.internal.processors.cluster.ChangeGlobalStateFinishMessage;
 import org.apache.ignite.internal.processors.cluster.GridClusterStateProcessor;
-import org.apache.ignite.internal.util.distributed.SingleNodeMessage;
 import org.apache.ignite.internal.util.future.IgniteFinishedFutureImpl;
 import org.apache.ignite.internal.util.lang.GridAbsPredicate;
 import org.apache.ignite.internal.util.lang.GridFunc;
@@ -159,7 +162,6 @@
 import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.resolveSnapshotWorkDirectory;
 import static org.apache.ignite.internal.processors.cache.verify.IdleVerifyUtility.GRID_NOT_IDLE_MSG;
 import static org.apache.ignite.internal.processors.diagnostic.DiagnosticProcessor.DEFAULT_TARGET_FOLDER;
-import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.RESTORE_CACHE_GROUP_SNAPSHOT_PREPARE;
 import static org.apache.ignite.testframework.GridTestUtils.assertContains;
 import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
 import static org.apache.ignite.testframework.GridTestUtils.runAsync;
@@ -3191,13 +3193,12 @@
     /** @throws Exception If fails. */
     @Test
     public void testSnapshotRestoreCancelAndStatus() throws Exception {
-        int keysCnt = 10_000;
+        int keysCnt = 2048;
         String snpName = "snapshot_25052021";
         String missingSnpName = "snapshot_MISSING";
 
-        IgniteEx ig = startGrids(2);
-
-        ig.cluster().state(ACTIVE);
+        IgniteEx ig = startGrid(getConfiguration(getTestIgniteInstanceName(0)).setSnapshotThreadPoolSize(1));
+        startGrid(1).cluster().state(ACTIVE);
 
         injectTestSystemOut();
 
@@ -3205,54 +3206,58 @@
 
         ig.snapshot().createSnapshot(snpName).get(getTestTimeout());
 
-        IgniteCache<Integer, Integer> cache1 = ig.cache(DEFAULT_CACHE_NAME);
+        int locPartsCnt = ig.cachex(DEFAULT_CACHE_NAME).context().topology().localPartitions().size();
 
-        cache1.destroy();
+        ig.destroyCache(DEFAULT_CACHE_NAME);
+        awaitPartitionMapExchange();
 
         CommandHandler h = new CommandHandler();
+        CountDownLatch ioStartLatch = new CountDownLatch(1);
+        IgniteSnapshotManager snpMgr = ig.context().cache().context().snapshotMgr();
 
-        TestRecordingCommunicationSpi spi = TestRecordingCommunicationSpi.spi(grid(1));
-
-        spi.blockMessages((node, msg) -> msg instanceof SingleNodeMessage &&
-            ((SingleNodeMessage<?>)msg).type() == RESTORE_CACHE_GROUP_SNAPSHOT_PREPARE.ordinal());
+        // Replace the IO factory in the snapshot manager so we have enough time to test the status command.
+        snpMgr.ioFactory(new SlowDownFileIoFactory(snpMgr.ioFactory(), getTestTimeout() / locPartsCnt, ioStartLatch));
 
         // Restore single cache group.
-        assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", snpName, "--start", DEFAULT_CACHE_NAME));
-        assertContains(log, testOut.toString(),
-            "Snapshot cache group restore operation started [snapshot=" + snpName + ", group(s)=" + DEFAULT_CACHE_NAME + ']');
+        IgniteFuture<Void> restoreFut = snpMgr.restoreSnapshot(snpName, Collections.singleton(DEFAULT_CACHE_NAME));
 
+        ioStartLatch.await(getTestTimeout(), TimeUnit.MILLISECONDS);
+        assertFalse(restoreFut.isDone());
+
+        // Check the status with a control command.
         assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", snpName, "--status"));
         assertContains(log, testOut.toString(),
             "Snapshot cache group restore operation is running [snapshot=" + snpName + ']');
 
-        // Check wrong snapshot name.
+        // Check "status" with the wrong snapshot name.
         assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", missingSnpName, "--status"));
         assertContains(log, testOut.toString(),
             "Snapshot cache group restore operation is NOT running [snapshot=" + missingSnpName + ']');
 
+        // Check "cancel" with the wrong snapshot name.
         assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", missingSnpName, "--cancel"));
         assertContains(log, testOut.toString(),
-            "Snapshot cache group restore operation is not in progress [snapshot=" + missingSnpName + ']');
+            "Snapshot cache group restore operation is NOT running [snapshot=" + missingSnpName + ']');
 
-        GridTestUtils.runAsync(() -> {
-            // Wait for the process to be interrupted.
-            AtomicReference<?> errRef = U.field((Object)U.field((Object)U.field(
-                grid(0).context().cache().context().snapshotMgr(), "restoreCacheGrpProc"), "opCtx"), "err");
-
-            waitForCondition(() -> errRef.get() != null, getTestTimeout());
-
-            spi.stopBlock();
-
-            return null;
-        });
-
+        // Cancel operation using control command.
         assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", snpName, "--cancel"));
         assertContains(log, testOut.toString(),
             "Snapshot cache group restore operation canceled [snapshot=" + snpName + ']');
 
+        GridTestUtils.assertThrowsAnyCause(log, () -> restoreFut.get(getTestTimeout()), IgniteCheckedException.class,
+            "Operation has been canceled by the user.");
+
+        // Make sure the context disappeared at node 1.
+        boolean ctxDisposed =
+            waitForCondition(() -> !grid(1).context().cache().context().snapshotMgr().isRestoring(), getTestTimeout());
+
+        assertTrue(ctxDisposed);
+
         assertEquals(EXIT_CODE_OK, execute(h, "--snapshot", "restore", snpName, "--status"));
         assertContains(log, testOut.toString(),
             "Snapshot cache group restore operation is NOT running [snapshot=" + snpName + ']');
+
+        assertNull(ig.cache(DEFAULT_CACHE_NAME));
     }
 
     /**
@@ -3354,4 +3359,57 @@
 
         return cnt;
     }
+
+    /** Test IO factory that slows down file creation. */
+    private static class SlowDownFileIoFactory implements FileIOFactory {
+        /** Delegated factory. */
+        private final FileIOFactory delegate;
+
+        /** Max slowdown interval. */
+        private final long maxTimeout;
+
+        /** Latch to notify when the first file will be created. */
+        private final CountDownLatch ioStartLatch;
+
+        /** Next file slowdown interval. */
+        private long timeout = 10;
+
+        /**
+         * @param delegate Delegated factory.
+         * @param maxTimeout Max slowdown interval.
+         * @param ioStartLatch Latch to notify when the first file will be created.
+         */
+        private SlowDownFileIoFactory(FileIOFactory delegate, long maxTimeout, CountDownLatch ioStartLatch) {
+            this.delegate = delegate;
+            this.maxTimeout = maxTimeout;
+            this.ioStartLatch = ioStartLatch;
+        }
+
+        /** {@inheritDoc} */
+        @Override public FileIO create(File file, OpenOption... modes) throws IOException {
+            try {
+                if (ioStartLatch.getCount() > 0)
+                    ioStartLatch.countDown();
+
+                long currTimeout = maxTimeout;
+
+                synchronized (this) {
+                    if (timeout < maxTimeout) {
+                        currTimeout = timeout;
+
+                        timeout += timeout;
+                    }
+                }
+
+                U.sleep(currTimeout);
+
+                return delegate.create(file, modes);
+            }
+            catch (IgniteInterruptedCheckedException e) {
+                Thread.currentThread().interrupt();
+
+                throw new RuntimeException(e);
+            }
+        }
+    }
 }
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/KillCommandsCommandShTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/KillCommandsCommandShTest.java
index ed19f72..7c844ad 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/KillCommandsCommandShTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/KillCommandsCommandShTest.java
@@ -24,11 +24,13 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInterruptedCheckedException;
 import org.apache.ignite.internal.TestRecordingCommunicationSpi;
+import org.apache.ignite.internal.commandline.consistency.ConsistencyCommand;
 import org.apache.ignite.internal.processors.cache.distributed.near.GridNearGetRequest;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.visor.consistency.VisorConsistencyRepairTask;
@@ -224,11 +226,11 @@
     /** */
     @Test
     public void testCancelConsistencyTask() throws InterruptedException {
-        String consistencyCancheName = "consistencyCache";
+        String consistencyCacheName = "consistencyCache";
 
         CacheConfiguration<Integer, Integer> cfg = new CacheConfiguration<>();
 
-        cfg.setName(consistencyCancheName);
+        cfg.setName(consistencyCacheName);
         cfg.setBackups(SERVER_NODE_CNT - 1);
         cfg.setAffinity(new RendezvousAffinityFunction().setPartitions(1));
 
@@ -312,7 +314,11 @@
 
         injectTestSystemOut();
 
-        assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--consistency", "repair", consistencyCancheName, "0"));
+        assertEquals(EXIT_CODE_UNEXPECTED_ERROR,
+            execute("--consistency", "repair",
+                ConsistencyCommand.STRATEGY, ReadRepairStrategy.LWW.toString(),
+                ConsistencyCommand.PARTITION, "0",
+                ConsistencyCommand.CACHE, consistencyCacheName));
 
         assertContains(log, testOut.toString(), "Operation execution cancelled.");
         assertContains(log, testOut.toString(), VisorConsistencyRepairTask.NOTHING_FOUND);
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/SystemViewCommandTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/SystemViewCommandTest.java
index ca5470c..9fee8d3 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/SystemViewCommandTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/SystemViewCommandTest.java
@@ -115,6 +115,7 @@
 import static org.apache.ignite.internal.processors.service.IgniteServiceProcessor.SVCS_VIEW;
 import static org.apache.ignite.internal.processors.task.GridTaskProcessor.TASKS_VIEW;
 import static org.apache.ignite.internal.util.IgniteUtils.toStringSafe;
+import static org.apache.ignite.spi.systemview.view.SnapshotView.SNAPSHOT_SYS_VIEW;
 import static org.apache.ignite.testframework.GridTestUtils.assertContains;
 import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
 import static org.apache.ignite.transactions.TransactionConcurrency.OPTIMISTIC;
@@ -431,6 +432,7 @@
             "LOCAL_CACHE_GROUPS_IO",
             "SQL_QUERIES",
             "SCAN_QUERIES",
+            "SNAPSHOT",
             "NODE_ATTRIBUTES",
             "TABLES",
             "CLIENT_CONNECTIONS",
@@ -1108,6 +1110,18 @@
             getTestTimeout()));
     }
 
+    /** */
+    @Test
+    public void testSnapshotView() throws Exception {
+        int srvCnt = ignite0.cluster().forServers().nodes().size();
+
+        String snap0 = "testSnapshot0";
+
+        ignite0.snapshot().createSnapshot(snap0).get();
+
+        assertEquals(srvCnt, systemView(ignite0, SNAPSHOT_SYS_VIEW).size());
+    }
+
     /**
      * Execute query on given node.
      *
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteCache.java b/modules/core/src/main/java/org/apache/ignite/IgniteCache.java
index 1a6e782..85587f9 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteCache.java
@@ -44,6 +44,7 @@
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cache.CachePeekMode;
 import org.apache.ignite.cache.PartitionLossPolicy;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.Query;
 import org.apache.ignite.cache.query.QueryCursor;
@@ -188,10 +189,11 @@
      * <li>{@link IgniteCache#get} && {@link IgniteCache#getAsync}</li>
      * <li>{@link IgniteCache#getAll} && {@link IgniteCache#getAllAsync}</li>
      * </ul>
+     * @param strategy Read Repair strategy.
      * @return Cache with explicit consistency check on each read and repair if necessary.
      */
     @IgniteExperimental
-    public IgniteCache<K, V> withReadRepair();
+    public IgniteCache<K, V> withReadRepair(ReadRepairStrategy strategy);
 
     /**
      * Returns cache that will operate with binary objects.
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteServices.java b/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
index 769d322..b74143c 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
@@ -567,7 +567,11 @@
      * @param name Service name.
      * @param <T> Service type
      * @return Deployed service with specified name.
+     * @see ServiceConfiguration#setStatisticsEnabled(boolean)
+     * @deprecated Use the proxies like {@link #serviceProxy(String, Class, boolean)}. References to local services
+     * corrupt the service statistics and bring no real performance optimization.
      */
+    @Deprecated
     public <T> T service(String name);
 
     /**
@@ -576,28 +580,29 @@
      * @param name Service name.
      * @param <T> Service type.
      * @return all deployed services with specified name.
+     * @see ServiceConfiguration#setStatisticsEnabled(boolean)
+     * @deprecated Use the proxies like {@link #serviceProxy(String, Class, boolean)}. References to local services
+     * corrupt the service statistics and bring no real performance optimization.
      */
+    @Deprecated
     public <T> Collection<T> services(String name);
 
     /**
-     * Gets a remote handle on the service. If service is available locally,
-     * then local instance is returned, otherwise, a remote proxy is dynamically
-     * created and provided for the specified service.
+     * Gets a handle on remote or local service. The proxy is dynamically created and provided for the specified service.
      *
      * @param name Service name.
      * @param svcItf Interface for the service.
      * @param sticky Whether or not Ignite should always contact the same remote
      *      service or try to load-balance between services.
      * @param <T> Service type.
-     * @return Either proxy over remote service or local service if it is deployed locally.
+     * @return Proxy over service.
      * @throws IgniteException If failed to create service proxy.
      */
     public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky) throws IgniteException;
 
     /**
-     * Gets a remote handle on the service with timeout. If service is available locally,
-     * then local instance is returned and timeout ignored, otherwise, a remote proxy is dynamically
-     * created and provided for the specified service.
+     * Gets a handle on remote or local service with the timeout. The proxy is dynamically created and provided for the
+     * specified service.
      *
      * @param name Service name.
      * @param svcItf Interface for the service.
@@ -606,15 +611,15 @@
      * @param timeout If greater than 0 created proxy will wait for service availability only specified time,
      *  and will limit remote service invocation time.
      * @param <T> Service type.
-     * @return Either proxy over remote service or local service if it is deployed locally.
+     * @return Proxy over service.
      * @throws IgniteException If failed to create service proxy.
      */
     public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky, long timeout)
         throws IgniteException;
 
     /**
-     * Gets a remote handle on the service with the specified caller context. The proxy
-     * is dynamically created and provided for the specified service.
+     * Gets a handle on remote or local service with the specified caller context. The proxy is dynamically created and
+     * provided for the specified service.
      *
      * @param name Service name.
      * @param svcItf Interface for the service.
@@ -635,8 +640,8 @@
     ) throws IgniteException;
 
     /**
-     * Gets a remote handle on the service with the specified caller context and timeout.
-     * The proxy is dynamically created and provided for the specified service.
+     * Gets a handle on remote or local service with the specified caller context and the timeout. The proxy is
+     * dynamically created and provided for the specified service.
      *
      * @param name Service name.
      * @param svcItf Interface for the service.
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
index d85ea8e..f83c2a4 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
@@ -32,6 +32,7 @@
 import org.apache.ignite.configuration.DeploymentMode;
 import org.apache.ignite.configuration.DiskPageCompression;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineRecommender;
 import org.apache.ignite.internal.client.GridClient;
 import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshaller;
@@ -56,6 +57,7 @@
 import static org.apache.ignite.internal.LongJVMPauseDetector.DEFAULT_JVM_PAUSE_DETECTOR_THRESHOLD;
 import static org.apache.ignite.internal.LongJVMPauseDetector.DFLT_JVM_PAUSE_DETECTOR_LAST_EVENTS_COUNT;
 import static org.apache.ignite.internal.LongJVMPauseDetector.DFLT_JVM_PAUSE_DETECTOR_PRECISION;
+import static org.apache.ignite.internal.binary.BinaryArray.DFLT_IGNITE_USE_BINARY_ARRAYS;
 import static org.apache.ignite.internal.binary.streams.BinaryMemoryAllocator.DFLT_MARSHAL_BUFFERS_PER_THREAD_POOL_SIZE;
 import static org.apache.ignite.internal.binary.streams.BinaryMemoryAllocator.DFLT_MARSHAL_BUFFERS_RECHECK;
 import static org.apache.ignite.internal.cache.query.index.sorted.inline.InlineRecommender.DFLT_THROTTLE_INLINE_SIZE_CALCULATION;
@@ -1130,6 +1132,13 @@
         "This property is intended for integration or performance tests")
     public static final String IGNITE_PREFER_WAL_REBALANCE = "IGNITE_PREFER_WAL_REBALANCE";
 
+    /**
+     * Threshold of the checkpoint quantity since the last earliest checkpoint map snapshot.
+     * After this thresold is reached, a snapshot of the earliest checkpoint map will be captured.
+     * Default is {@code 5}.
+     */
+    public static final String IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD = "IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD";
+
     /** Ignite page memory concurrency level. */
     @SystemProperty(value = "Ignite page memory concurrency level", type = Integer.class)
     public static final String IGNITE_OFFHEAP_LOCK_CONCURRENCY_LEVEL = "IGNITE_OFFHEAP_LOCK_CONCURRENCY_LEVEL";
@@ -1893,7 +1902,9 @@
      * When enabled, node will wait until all of its data is backed up before shutting down.
      * Please note that it will completely prevent last node in cluster from shutting down if any caches exist
      * that have backups configured.
+     * @deprecated Use {@link ShutdownPolicy} instead.
      */
+    @Deprecated
     @IgniteExperimental
     @SystemProperty("Enables node to wait until all of its data is backed up before " +
         "shutting down. Please note that it will completely prevent last node in cluster from shutting down if any " +
@@ -2007,6 +2018,14 @@
     public static final String IGNITE_THROTTLE_INLINE_SIZE_CALCULATION = "IGNITE_THROTTLE_INLINE_SIZE_CALCULATION";
 
     /**
+     * Enables storage of typed arrays.
+     * The default value is {@link BinaryArray#DFLT_IGNITE_USE_BINARY_ARRAYS}.
+     */
+    @SystemProperty(value = "Flag to enable store of array in binary format and keep component type",
+        defaults = "" + DFLT_IGNITE_USE_BINARY_ARRAYS)
+    public static final String IGNITE_USE_BINARY_ARRAYS = "IGNITE_USE_BINARY_ARRAYS";
+
+    /**
      * Enforces singleton.
      */
     private IgniteSystemProperties() {
diff --git a/modules/core/src/main/java/org/apache/ignite/cache/ReadRepairStrategy.java b/modules/core/src/main/java/org/apache/ignite/cache/ReadRepairStrategy.java
new file mode 100644
index 0000000..4d80037
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/cache/ReadRepairStrategy.java
@@ -0,0 +1,89 @@
+/*
+ * 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.cache;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.lang.IgniteExperimental;
+
+/**
+ * Read repair strategies.
+ *
+ * @see IgniteCache#withReadRepair(ReadRepairStrategy) for details.
+ */
+@IgniteExperimental
+public enum ReadRepairStrategy {
+    /** Last write (the newest entry) wins.
+     * <p>
+     * May cause {@link IgniteException} when fix is impossible (unable to detect the newest entry):
+     * <ul>
+     * <li>Null(s) found as well as non-null values for the same key.
+     * <p>
+     * Null (missed entry) has no version, so, it can not be compared with the versioned entry.</li>
+     * <li>Entries with the same version have different values.</li>
+     * </ul>
+     */
+    LWW("LWW"),
+
+    /** Value from the primary node wins. */
+    PRIMARY("PRIMARY"),
+
+    /** The relative majority, any value found more times than any other wins.
+     * <p>
+     * Works for an even number of copies (which is typical of Ignite) instead of an absolute majority.
+     * <p>
+     * May cause {@link IgniteException} when unable to detect value found more times than any other.
+     * <p>
+     * For example, when we have 5 copies (4 backups) and value `A` found twice, but `X`,`Y` and `Z` only once, `A` wins.
+     * But, when `A` found twice, as well as `B`, and `X` only once, the strategy unable to determine the winner.
+     * <p>
+     * When we have 4 copies (3 backups), any value found two or more times, when others are found only once, is the winner.
+     */
+    RELATIVE_MAJORITY("RELATIVE_MAJORITY"),
+
+    /** Inconsistent entries will be removed. */
+    REMOVE("REMOVE"),
+
+    /** Only check will be performed. */
+    CHECK_ONLY("CHECK_ONLY");
+
+    /** Strategy name. */
+    private final String name;
+
+    /**
+     * @param name Name.
+     */
+    ReadRepairStrategy(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Provides strategy by name.
+     *
+     * @param name Text.
+     * @return Read Repair strategy.
+     */
+    public static ReadRepairStrategy fromString(String name) {
+        for (ReadRepairStrategy strategy : values()) {
+            if (strategy.name.equalsIgnoreCase(name))
+                return strategy;
+        }
+
+        throw new IllegalArgumentException("Unknown strategy: " + name);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/cdc/CdcConsumer.java b/modules/core/src/main/java/org/apache/ignite/cdc/CdcConsumer.java
index b0bf648..1a13465 100644
--- a/modules/core/src/main/java/org/apache/ignite/cdc/CdcConsumer.java
+++ b/modules/core/src/main/java/org/apache/ignite/cdc/CdcConsumer.java
@@ -35,7 +35,7 @@
  *     <li>Stop of the consumer {@link #stop()}.</li>
  * </ul>
  *
- * In case consumer implementation wants to user {@link IgniteLogger}, please, use, {@link LoggerResource} annotation:
+ * In case consumer implementation wants to use {@link IgniteLogger}, please, use, {@link LoggerResource} annotation:
  * <pre>
  * public class ChangeDataCaptureConsumer implements ChangeDataCaptureConsumer {
  *     &#64;LoggerResource
diff --git a/modules/core/src/main/java/org/apache/ignite/cdc/CdcLoader.java b/modules/core/src/main/java/org/apache/ignite/cdc/CdcLoader.java
index b3bc583..ca7b4f6 100644
--- a/modules/core/src/main/java/org/apache/ignite/cdc/CdcLoader.java
+++ b/modules/core/src/main/java/org/apache/ignite/cdc/CdcLoader.java
@@ -19,11 +19,13 @@
 
 import java.net.URL;
 import java.util.Collection;
+import java.util.Map;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.cdc.CdcMain;
 import org.apache.ignite.internal.processors.resource.GridSpringResourceContext;
 import org.apache.ignite.internal.util.spring.IgniteSpringHelper;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
 import org.apache.ignite.lang.IgniteExperimental;
@@ -47,29 +49,25 @@
 
         IgniteSpringHelper spring = SPRING.create(false);
 
-        IgniteBiTuple<Collection<IgniteConfiguration>, ? extends GridSpringResourceContext> cfgTuple =
-            spring.loadConfigurations(cfgUrl);
+        IgniteBiTuple<Map<Class<?>, Collection>, ? extends GridSpringResourceContext> cfgs =
+            spring.loadBeans(cfgUrl, IgniteConfiguration.class, CdcConfiguration.class);
 
-        if (cfgTuple.get1().size() > 1) {
+        Collection<IgniteConfiguration> igniteCfgs = cfgs.get1().get(IgniteConfiguration.class);
+
+        if (F.size(igniteCfgs) != 1) {
             throw new IgniteCheckedException(
-                "Exact 1 IgniteConfiguration should be defined. Found " + cfgTuple.get1().size()
+                "Exact 1 IgniteConfiguration should be defined. Found " + F.size(igniteCfgs)
             );
         }
 
-        IgniteBiTuple<Collection<CdcConfiguration>, ? extends GridSpringResourceContext> cdcCfgs =
-            spring.loadConfigurations(cfgUrl, CdcConfiguration.class);
+        Collection<CdcConfiguration> cdcCfgs = cfgs.get1().get(CdcConfiguration.class);
 
-        if (cdcCfgs.get1().size() > 1) {
+        if (F.size(cdcCfgs) != 1) {
             throw new IgniteCheckedException(
-                "Exact 1 CaptureDataChangeConfiguration configuration should be defined. " +
-                    "Found " + cdcCfgs.get1().size()
+                "Exact 1 CaptureDataChangeConfiguration configuration should be defined. Found " + F.size(cdcCfgs)
             );
         }
 
-        return new CdcMain(
-            cfgTuple.get1().iterator().next(),
-            cfgTuple.get2(),
-            cdcCfgs.get1().iterator().next()
-        );
+        return new CdcMain(F.first(igniteCfgs), cfgs.get2(), F.first(cdcCfgs));
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientOperationType.java b/modules/core/src/main/java/org/apache/ignite/client/ClientOperationType.java
new file mode 100644
index 0000000..d5ac4b6
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientOperationType.java
@@ -0,0 +1,217 @@
+/*
+ * 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;
+
+import java.util.Set;
+import org.apache.ignite.cache.query.ContinuousQuery;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cluster.ClusterState;
+
+/**
+ * Client operation type.
+ */
+public enum ClientOperationType {
+    /**
+     * Create cache ({@link IgniteClient#createCache(String)},
+     * {@link IgniteClient#createCache(ClientCacheConfiguration)}).
+     */
+    CACHE_CREATE,
+
+    /**
+     * Get or create cache ({@link IgniteClient#getOrCreateCache(String)},
+     * {@link IgniteClient#getOrCreateCache(ClientCacheConfiguration)}).
+     */
+    CACHE_GET_OR_CREATE,
+
+    /**
+     * Get cache names ({@link IgniteClient#cacheNames()}).
+     */
+    CACHE_GET_NAMES,
+
+    /**
+     * Destroy cache ({@link IgniteClient#destroyCache(String)}).
+     */
+    CACHE_DESTROY,
+
+    /**
+     * Get value from cache ({@link ClientCache#get(Object)}).
+     */
+    CACHE_GET,
+
+    /**
+     * Put value to cache ({@link ClientCache#put(Object, Object)}).
+     */
+    CACHE_PUT,
+
+    /**
+     * Determines if the cache contains a key ({@link ClientCache#containsKey(Object)}).
+     */
+    CACHE_CONTAINS_KEY,
+
+    /**
+     * Determines if the cache contains multiple keys ({@link ClientCache#containsKeys}).
+     */
+    CACHE_CONTAINS_KEYS,
+
+    /**
+     * Get cache configuration ({@link ClientCache#getConfiguration()}).
+     */
+    CACHE_GET_CONFIGURATION,
+
+    /**
+     * Get cache size ({@link ClientCache#size}).
+     */
+    CACHE_GET_SIZE,
+
+    /**
+     * Put values to cache ({@link ClientCache#putAll}).
+     */
+    CACHE_PUT_ALL,
+
+    /**
+     * Get values from cache ({@link ClientCache#getAll}).
+     */
+    CACHE_GET_ALL,
+
+    /**
+     * Replace cache value ({@link ClientCache#replace(Object, Object)},
+     * {@link ClientCache#replace(Object, Object, Object)}).
+     */
+    CACHE_REPLACE,
+
+    /**
+     * Remove entry from cache ({@link ClientCache#remove(Object)}, {@link ClientCache#remove(Object, Object)}).
+     */
+    CACHE_REMOVE_ONE,
+
+    /**
+     * Remove entries from cache ({@link ClientCache#removeAll(Set)}).
+     */
+    CACHE_REMOVE_MULTIPLE,
+
+    /**
+     * Remove everything from cache ({@link ClientCache#removeAll()}).
+     */
+    CACHE_REMOVE_EVERYTHING,
+
+    /**
+     * Clear cache entry ({@link ClientCache#clear(Object)} ).
+     */
+    CACHE_CLEAR_ONE,
+
+    /**
+     * Clear multiple cache entries ({@link ClientCache#clearAll(Set)}).
+     */
+    CACHE_CLEAR_MULTIPLE,
+
+    /**
+     * Clear entire cache ({@link ClientCache#clear()}).
+     */
+    CACHE_CLEAR_EVERYTHING,
+
+    /**
+     * Get and put ({@link ClientCache#getAndPut(Object, Object)}).
+     */
+    CACHE_GET_AND_PUT,
+
+    /**
+     * Get and remove ({@link ClientCache#getAndRemove(Object)}).
+     */
+    CACHE_GET_AND_REMOVE,
+
+    /**
+     * Get and replace ({@link ClientCache#getAndReplace(Object, Object)}).
+     */
+    CACHE_GET_AND_REPLACE,
+
+    /**
+     * Put if absent ({@link ClientCache#putIfAbsent(Object, Object)}).
+     */
+    CACHE_PUT_IF_ABSENT,
+
+    /**
+     * Get and put if absent ({@link ClientCache#getAndPutIfAbsent(Object, Object)}).
+     */
+    CACHE_GET_AND_PUT_IF_ABSENT,
+
+    /**
+     * Scan query ({@link ClientCache#query(Query)}).
+     */
+    QUERY_SCAN,
+
+    /**
+     * SQL query ({@link ClientCache#query(SqlFieldsQuery)}).
+     */
+    QUERY_SQL,
+
+    /**
+     * Continuous query ({@link ClientCache#query(ContinuousQuery, ClientDisconnectListener)}).
+     */
+    QUERY_CONTINUOUS,
+
+    /**
+     * Start transaction ({@link ClientTransactions#txStart}).
+     */
+    TRANSACTION_START,
+
+    /**
+     * Get cluster state ({@link ClientCluster#state()}).
+     */
+    CLUSTER_GET_STATE,
+
+    /**
+     * Change cluster state ({@link ClientCluster#state(ClusterState)}).
+     */
+    CLUSTER_CHANGE_STATE,
+
+    /**
+     * Get cluster WAL state ({@link ClientCluster#isWalEnabled(String)}).
+     */
+    CLUSTER_GET_WAL_STATE,
+
+    /**
+     * Change cluster WAL state ({@link ClientCluster#enableWal(String)}, {@link ClientCluster#disableWal(String)}).
+     */
+    CLUSTER_CHANGE_WAL_STATE,
+
+    /**
+     * Get cluster nodes ({@link ClientCluster#nodes()}).
+     */
+    CLUSTER_GROUP_GET_NODES,
+
+    /**
+     * Execute compute task ({@link ClientCompute#execute(String, Object)}).
+     */
+    COMPUTE_TASK_EXECUTE,
+
+    /**
+     * Invoke service.
+     */
+    SERVICE_INVOKE,
+
+    /**
+     * Get service descriptors ({@link ClientServices#serviceDescriptors()}).
+     */
+    SERVICE_GET_DESCRIPTORS,
+
+    /**
+     * Get service descriptor ({@link ClientServices#serviceDescriptor(String)}).
+     */
+    SERVICE_GET_DESCRIPTOR
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryAllPolicy.java
similarity index 72%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/main/java/org/apache/ignite/client/ClientRetryAllPolicy.java
index a244658..a65e50e 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryAllPolicy.java
@@ -15,16 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
-
-import org.apache.ignite.cache.CacheMode;
+package org.apache.ignite.client;
 
 /**
- *
+ * Retry policy that always returns {@code true}.
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+public class ClientRetryAllPolicy implements ClientRetryPolicy {
+    /** */
+    private static final long serialVersionUID = 0L;
+
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override public boolean shouldRetry(ClientRetryPolicyContext context) {
+        return true;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryNonePolicy.java
similarity index 72%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/main/java/org/apache/ignite/client/ClientRetryNonePolicy.java
index a244658..0618cda 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryNonePolicy.java
@@ -15,16 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
-
-import org.apache.ignite.cache.CacheMode;
+package org.apache.ignite.client;
 
 /**
- *
+ * Retry policy that always returns {@code false}.
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+public class ClientRetryNonePolicy implements ClientRetryPolicy {
+    /** */
+    private static final long serialVersionUID = 0L;
+
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override public boolean shouldRetry(ClientRetryPolicyContext context) {
+        return false;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicy.java b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicy.java
new file mode 100644
index 0000000..d39ad0c
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicy.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+/**
+ * Client retry policy determines whether client operations that have failed due to a connection issue should be retried.
+ */
+public interface ClientRetryPolicy extends Serializable {
+    /**
+     * Gets a value indicating whether a client operation that has failed due to a connection issue should be retried.
+     *
+     * @param context Context.
+     * @return {@code true} if the operation should be retried on another connection, {@code false} otherwise.
+     */
+    public boolean shouldRetry(ClientRetryPolicyContext context);
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicyContext.java b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicyContext.java
new file mode 100644
index 0000000..32e51c7
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryPolicyContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import org.apache.ignite.configuration.ClientConfiguration;
+
+/**
+ * Retry policy context. See {@link ClientRetryPolicy#shouldRetry}.
+ */
+public interface ClientRetryPolicyContext {
+    /**
+     * Gets the client configuration.
+     *
+     * @return Client configuration.
+     */
+    public ClientConfiguration configuration();
+
+    /**
+     * Gets the operation type.
+     *
+     * @return Operation type.
+     */
+    public ClientOperationType operation();
+
+    /**
+     * Gets the current iteration number (zero-based).
+     *
+     * @return Zero-based iteration counter.
+     */
+    public int iteration();
+
+    /**
+     * Gets the connection exception that caused current retry iteration.
+     *
+     * @return Exception.
+     */
+    public ClientConnectionException exception();
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientRetryReadPolicy.java b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryReadPolicy.java
new file mode 100644
index 0000000..c0c50ef
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientRetryReadPolicy.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.client;
+
+/**
+ * Retry policy that returns true for all read-only operations that do not modify data.
+ */
+public class ClientRetryReadPolicy implements ClientRetryPolicy {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override public boolean shouldRetry(ClientRetryPolicyContext context) {
+        switch (context.operation()) {
+            case CACHE_GET_NAMES:
+            case CACHE_GET:
+            case CACHE_CONTAINS_KEY:
+            case CACHE_CONTAINS_KEYS:
+            case CACHE_GET_CONFIGURATION:
+            case CACHE_GET_SIZE:
+            case CACHE_GET_ALL:
+            case QUERY_SCAN:
+            case QUERY_CONTINUOUS:
+            case CLUSTER_GET_STATE:
+            case CLUSTER_GET_WAL_STATE:
+            case CLUSTER_GROUP_GET_NODES:
+            case SERVICE_GET_DESCRIPTORS:
+            case SERVICE_GET_DESCRIPTOR:
+                return true;
+
+            default:
+                return false;
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientServiceDescriptor.java b/modules/core/src/main/java/org/apache/ignite/client/ClientServiceDescriptor.java
new file mode 100644
index 0000000..1feba70
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientServiceDescriptor.java
@@ -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.
+ */
+
+package org.apache.ignite.client;
+
+import java.util.UUID;
+import org.apache.ignite.platform.PlatformType;
+import org.apache.ignite.services.Service;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Descriptor of {@link Service}.
+ */
+public interface ClientServiceDescriptor {
+    /**
+     * Gets service name.
+     *
+     * @return Service name.
+     */
+    public String name();
+
+    /**
+     * Gets service class.
+     *
+     * @return Service class.
+     */
+    public String serviceClass();
+
+    /**
+     * Gets maximum allowed total number of deployed services in the grid, {@code 0} for unlimited.
+     *
+     * @return Maximum allowed total number of deployed services in the grid, {@code 0} for unlimited.
+     */
+    public int totalCount();
+
+    /**
+     * Gets maximum allowed number of deployed services on each node, {@code 0} for unlimited.
+     *
+     * @return Maximum allowed total number of deployed services on each node, {@code 0} for unlimited.
+     */
+    public int maxPerNodeCount();
+
+    /**
+     * Gets cache name used for key-to-node affinity calculation. This parameter is optional
+     * and is set only when key-affinity service was deployed.
+     *
+     * @return Cache name, possibly {@code null}.
+     */
+    @Nullable public String cacheName();
+
+    /**
+     * Gets ID of grid node that initiated the service deployment.
+     *
+     * @return ID of grid node that initiated the service deployment.
+     */
+    public UUID originNodeId();
+
+    /**
+     * Gets platform type.
+     *
+     * @return Platform type.
+     */
+    public PlatformType platformType();
+}
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
index 1be013a..7f30aec 100644
--- a/modules/core/src/main/java/org/apache/ignite/client/ClientServices.java
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientServices.java
@@ -17,6 +17,10 @@
 
 package org.apache.ignite.client;
 
+import java.util.Collection;
+import org.apache.ignite.lang.IgniteExperimental;
+import org.apache.ignite.services.ServiceCallContext;
+
 /**
  * Thin client services facade.
  */
@@ -55,4 +59,53 @@
      * @return Proxy over remote service.
      */
     public <T> T serviceProxy(String name, Class<? super T> svcItf, long timeout);
+
+    /**
+     * Gets metadata about all deployed services in the grid.
+     *
+     * @return Metadata about all deployed services in the grid.
+     */
+    public Collection<ClientServiceDescriptor> serviceDescriptors();
+
+    /**
+     * Gets metadata about deployed services in the grid.
+     *
+     * @param name Service name.
+     * @return Metadata about all deployed services in the grid.
+     */
+    public ClientServiceDescriptor serviceDescriptor(String name);
+
+    /**
+     * Gets a remote handle on the service with the specified caller context.
+     * <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 callCtx Service call context.
+     * @param <T> Service type.
+     * @return Proxy over remote service.
+     * @see ServiceCallContext
+     */
+    @IgniteExperimental
+    public <T> T serviceProxy(String name, Class<? super T> svcItf, ServiceCallContext callCtx);
+
+    /**
+     * Gets a remote handle on the service with the specified caller context and 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 callCtx Service call context.
+     * @param timeout If greater than 0 created proxy will wait for service availability only specified time,
+     *  and will limit remote service invocation time.
+     * @param <T> Service type.
+     * @return Proxy over remote service.
+     * @see ServiceCallContext
+     */
+    @IgniteExperimental
+    public <T> T serviceProxy(String name, Class<? super T> svcItf, ServiceCallContext callCtx, long timeout);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/ClientConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/ClientConfiguration.java
index 9f8e7f2..1dc6e66 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/ClientConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/ClientConfiguration.java
@@ -25,6 +25,8 @@
 import javax.cache.configuration.Factory;
 import javax.net.ssl.SSLContext;
 import org.apache.ignite.client.ClientAddressFinder;
+import org.apache.ignite.client.ClientRetryAllPolicy;
+import org.apache.ignite.client.ClientRetryPolicy;
 import org.apache.ignite.client.SslMode;
 import org.apache.ignite.client.SslProtocol;
 import org.apache.ignite.internal.client.thin.TcpIgniteClient;
@@ -122,6 +124,9 @@
     /** Retry limit. */
     private int retryLimit = 0;
 
+    /** Retry policy. */
+    private ClientRetryPolicy retryPolicy = new ClientRetryAllPolicy();
+
     /** Executor for async operations continuations. */
     private Executor asyncContinuationExecutor;
 
@@ -582,6 +587,32 @@
         return this;
     }
 
+    /**
+     * Gets the retry policy.
+     *
+     * @return Retry policy.
+     */
+    public ClientRetryPolicy getRetryPolicy() {
+        return retryPolicy;
+    }
+
+    /**
+     * Sets the retry policy. When a request fails due to a connection error, and multiple server connections
+     * are available, Ignite will retry the request if the specified policy allows it.
+     * <p />
+     * When {@link ClientConfiguration#retryLimit} is set, retry count will be limited even if the specified policy returns {@code true}.
+     * <p />
+     * Default is {@link ClientRetryAllPolicy}.
+     *
+     * @param retryPolicy Retry policy.
+     * @return {@code this} for chaining.
+     */
+    public ClientConfiguration setRetryPolicy(ClientRetryPolicy retryPolicy) {
+        this.retryPolicy = retryPolicy;
+
+        return this;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return S.toString(ClientConfiguration.class, this);
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
index 3f3b09a..2865bab4 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
@@ -20,6 +20,7 @@
 import org.apache.ignite.DataRegionMetrics;
 import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
 import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.mem.MemoryAllocator;
 import org.apache.ignite.mxbean.DataRegionMetricsMXBean;
 import org.apache.ignite.mxbean.MetricsMxBean;
 import org.jetbrains.annotations.Nullable;
@@ -152,6 +153,9 @@
     /** Warm-up configuration. */
     @Nullable private WarmUpConfiguration warmUpCfg;
 
+    /** Memory allocator. */
+    @Nullable private MemoryAllocator memoryAllocator = null;
+
     /**
      * Gets data region name.
      *
@@ -245,6 +249,25 @@
     }
 
     /**
+     * @return Memory allocator instance.
+     */
+    @Nullable public MemoryAllocator getMemoryAllocator() {
+        return memoryAllocator;
+    }
+
+    /**
+     * Sets memory allocator. If not specified, default, based on {@code Unsafe} allocator will be used.
+     *
+     * @param allocator Memory allocator instance.
+     * @return {@code this} for chaining.
+     */
+    public DataRegionConfiguration setMemoryAllocator(MemoryAllocator allocator) {
+        memoryAllocator = allocator;
+
+        return this;
+    }
+
+    /**
      * Gets memory pages eviction mode. If {@link DataPageEvictionMode#DISABLED} is used (default) then an out of
      * memory exception will be thrown if the memory region usage, defined by this data region, goes beyond its
      * capacity which is {@link #getMaxSize()}.
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
index 045b8a6..6e4801b 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
@@ -28,6 +28,7 @@
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteExperimental;
+import org.apache.ignite.mem.MemoryAllocator;
 import org.apache.ignite.mxbean.MetricsMxBean;
 import org.jetbrains.annotations.Nullable;
 
@@ -87,12 +88,6 @@
         (long)(DFLT_DATA_REGION_FRACTION * U.getTotalMemoryAvailable()),
         DFLT_DATA_REGION_INITIAL_SIZE);
 
-    /** Default initial size of a memory chunk for the system cache (40 MB). */
-    private static final long DFLT_SYS_REG_INIT_SIZE = 40L * 1024 * 1024;
-
-    /** Default max size of a memory chunk for the system cache (100 MB). */
-    private static final long DFLT_SYS_REG_MAX_SIZE = 100L * 1024 * 1024;
-
     /** Default memory page size. */
     public static final int DFLT_PAGE_SIZE = 4 * 1024;
 
@@ -195,12 +190,6 @@
     /** Value used to indicate the use of half of the {@link #getMaxWalArchiveSize}. */
     public static final long HALF_MAX_WAL_ARCHIVE_SIZE = -1;
 
-    /** Initial size of a memory chunk reserved for system cache. */
-    private long sysRegionInitSize = DFLT_SYS_REG_INIT_SIZE;
-
-    /** Maximum size of a memory chunk reserved for system cache. */
-    private long sysRegionMaxSize = DFLT_SYS_REG_MAX_SIZE;
-
     /** Memory page size. */
     private int pageSize = IgniteSystemProperties.getInteger(
         IGNITE_DEFAULT_DATA_STORAGE_PAGE_SIZE, 0);
@@ -209,6 +198,9 @@
     private int concLvl;
 
     /** Configuration of default data region. */
+    private SystemDataRegionConfiguration sysDataRegConf = new SystemDataRegionConfiguration();
+
+    /** Configuration of default data region. */
     private DataRegionConfiguration dfltDataRegConf = new DataRegionConfiguration();
 
     /** Data regions. */
@@ -307,9 +299,7 @@
     @IgniteExperimental
     private long walForceArchiveTimeout = -1;
 
-    /**
-     * If true, threads that generate dirty pages too fast during ongoing checkpoint will be throttled.
-     */
+    /** If true, threads that generate dirty pages too fast during ongoing checkpoint will be throttled. */
     private boolean writeThrottlingEnabled = DFLT_WRITE_THROTTLING_ENABLED;
 
     /**
@@ -348,6 +338,9 @@
     /** Minimum size of wal archive folder, in bytes. */
     private long minWalArchiveSize = HALF_MAX_WAL_ARCHIVE_SIZE;
 
+    /** Default memory allocator for all data regions. */
+    @Nullable private MemoryAllocator memoryAllocator = null;
+
     /**
      * Creates valid durable memory configuration with all default values.
      */
@@ -359,23 +352,29 @@
      * Initial size of a data region reserved for system cache.
      *
      * @return Size in bytes.
+     * @deprecated use {@link SystemDataRegionConfiguration#getInitialSize}.
      */
+    @Deprecated
     public long getSystemRegionInitialSize() {
-        return sysRegionInitSize;
+        if (sysDataRegConf == null)
+            sysDataRegConf = new SystemDataRegionConfiguration();
+
+        return sysDataRegConf.getInitialSize();
     }
 
     /**
      * Sets initial size of a data region reserved for system cache.
      *
-     * Default value is {@link #DFLT_SYS_REG_INIT_SIZE}
-     *
      * @param sysRegionInitSize Size in bytes.
      * @return {@code this} for chaining.
+     * @deprecated use {@link SystemDataRegionConfiguration#setInitialSize(long)}.
      */
+    @Deprecated
     public DataStorageConfiguration setSystemRegionInitialSize(long sysRegionInitSize) {
-        A.ensure(sysRegionInitSize > 0, "System region initial size can not be less zero.");
+        if (sysDataRegConf == null)
+            sysDataRegConf = new SystemDataRegionConfiguration();
 
-        this.sysRegionInitSize = sysRegionInitSize;
+        sysDataRegConf.setInitialSize(sysRegionInitSize);
 
         return this;
     }
@@ -384,24 +383,30 @@
      * Maximum data region size reserved for system cache.
      *
      * @return Size in bytes.
+     * @deprecated use {@link SystemDataRegionConfiguration#getMaxSize()}.
      */
+    @Deprecated
     public long getSystemRegionMaxSize() {
-        return sysRegionMaxSize;
+        if (sysDataRegConf == null)
+            sysDataRegConf = new SystemDataRegionConfiguration();
+
+        return sysDataRegConf.getMaxSize();
     }
 
     /**
      * Sets maximum data region size reserved for system cache. The total size should not be less than 10 MB
      * due to internal data structures overhead.
      *
-     * Default value is {@link #DFLT_SYS_REG_MAX_SIZE}.
-     *
      * @param sysRegionMaxSize Maximum size in bytes for system cache data region.
      * @return {@code this} for chaining.
+     * @deprecated use {@link SystemDataRegionConfiguration#setMaxSize(long)}.
      */
+    @Deprecated
     public DataStorageConfiguration setSystemRegionMaxSize(long sysRegionMaxSize) {
-        A.ensure(sysRegionMaxSize > 0, "System region max size can not be less zero.");
+        if (sysDataRegConf == null)
+            sysDataRegConf = new SystemDataRegionConfiguration();
 
-        this.sysRegionMaxSize = sysRegionMaxSize;
+        sysDataRegConf.setMaxSize(sysRegionMaxSize);
 
         return this;
     }
@@ -494,7 +499,7 @@
     }
 
     /**
-     * Overrides configuration of default data region which is created automatically.
+     * Overrides configuration of default data region which has been created automatically.
      *
      * @param dfltDataRegConf Default data region configuration.
      * @return {@code this} for chaining.
@@ -506,6 +511,27 @@
     }
 
     /**
+     * Configuration of system data region.
+     *
+     * @return Configuration of system data region.
+     */
+    public SystemDataRegionConfiguration getSystemDataRegionConfiguration() {
+        return sysDataRegConf;
+    }
+
+    /**
+     * Overrides configuration of system data region which has been created automatically.
+     *
+     * @param sysDataRegConf System data region configuration.
+     * @return {@code this} for chaining.
+     */
+    public DataStorageConfiguration setSystemDataRegionConfiguration(SystemDataRegionConfiguration sysDataRegConf) {
+        this.sysDataRegConf = sysDataRegConf;
+
+        return this;
+    }
+
+    /**
      * @return A path the root directory where the Persistent Store will persist data and indexes.
      */
     public String getStoragePath() {
@@ -1342,6 +1368,27 @@
         return this;
     }
 
+    /**
+     * @return Memory allocator instance.
+     */
+    @Nullable public MemoryAllocator getMemoryAllocator() {
+        return memoryAllocator;
+    }
+
+    /**
+     * Sets default memory allocator for all memory regions. If not specified, default, based on {@code Unsafe}
+     * allocator will be used. Allocator can be overrided for data region using
+     * {@link DataRegionConfiguration#setMemoryAllocator(MemoryAllocator)}
+     *
+     * @param allocator Memory allocator instance.
+     * @return {@code this} for chaining.
+     */
+    public DataStorageConfiguration setMemoryAllocator(MemoryAllocator allocator) {
+        memoryAllocator = allocator;
+
+        return this;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return S.toString(DataStorageConfiguration.class, this);
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/IgniteConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/IgniteConfiguration.java
index ea5d316..bef7031 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/IgniteConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/IgniteConfiguration.java
@@ -225,9 +225,12 @@
     /** Default value for cache sanity check enabled flag. */
     public static final boolean DFLT_CACHE_SANITY_CHECK_ENABLED = true;
 
-    /** Default relative working directory path for snapshot operation result. */
+    /** Default relative working directory path for snapshot operation result. The default directory is <tt>snapshots</tt>. */
     public static final String DFLT_SNAPSHOT_DIRECTORY = "snapshots";
 
+    /** Default number of threads to perform snapshot operations. The default value is <tt>4</tt>. */
+    public static final int DFLT_SNAPSHOT_THREAD_POOL_SIZE = 4;
+
     /** Default value for late affinity assignment flag. */
     @Deprecated
     public static final boolean DFLT_LATE_AFF_ASSIGNMENT = true;
@@ -564,6 +567,9 @@
      */
     private String snapshotPath = DFLT_SNAPSHOT_DIRECTORY;
 
+    /** Total number of threads to perform snapshot operation. By default, the {@link #DFLT_SNAPSHOT_THREAD_POOL_SIZE} is used. */
+    private int snapshotThreadPoolSize = DFLT_SNAPSHOT_THREAD_POOL_SIZE;
+
     /** Active on start flag. */
     @Deprecated
     private boolean activeOnStart = DFLT_ACTIVE_ON_START;
@@ -723,6 +729,7 @@
         segResolveAttempts = cfg.getSegmentationResolveAttempts();
         segResolvers = cfg.getSegmentationResolvers();
         snapshotPath = cfg.getSnapshotPath();
+        snapshotThreadPoolSize = cfg.getSnapshotThreadPoolSize();
         sndRetryCnt = cfg.getNetworkSendRetryCount();
         sndRetryDelay = cfg.getNetworkSendRetryDelay();
         sqlConnCfg = cfg.getSqlConnectorConfiguration();
@@ -1747,6 +1754,25 @@
     }
 
     /**
+     * @return Total number of threads to perform snapshot operation. By default,
+     * the {@link #DFLT_SNAPSHOT_THREAD_POOL_SIZE} is used.
+     */
+    public int getSnapshotThreadPoolSize() {
+        return snapshotThreadPoolSize;
+    }
+
+    /**
+     * @param snapshotThreadPoolSize Total number of threads to perform snapshot operation. By default,
+     * the {@link #DFLT_SNAPSHOT_THREAD_POOL_SIZE} is used.
+     * @return {@code this} for chaining.
+     */
+    public IgniteConfiguration setSnapshotThreadPoolSize(int snapshotThreadPoolSize) {
+        this.snapshotThreadPoolSize = snapshotThreadPoolSize;
+
+        return this;
+    }
+
+    /**
      * Gets Max count of threads can be used at rebalancing.
      * Minimum is 1.
      * @return count.
@@ -2177,7 +2203,7 @@
      * on arrive to mapped node. This approach suits well for large amount of small
      * jobs (which is a wide-spread use case). User still can control the number
      * of concurrent jobs by setting maximum thread pool size defined by
-     * IgniteConfiguration.getPublicThreadPoolSize() configuration property.
+     * {@link IgniteConfiguration#getPublicThreadPoolSize()} configuration property.
      *
      * @return Grid collision SPI implementation or {@code null} to use default implementation.
      */
@@ -3160,7 +3186,7 @@
     }
 
     /**
-     * @return By default the relative {@link #DFLT_SNAPSHOT_DIRECTORY} is used. The value can be
+     * @return By default, the relative {@link #DFLT_SNAPSHOT_DIRECTORY} is used. The value can be
      * configured as relative path starting from the Ignites {@link #getWorkDirectory()} or
      * the value can be represented as an absolute snapshot working path.
      */
@@ -3169,7 +3195,7 @@
     }
 
     /**
-     * @param snapshotPath By default the relative {@link #DFLT_SNAPSHOT_DIRECTORY} is used.
+     * @param snapshotPath By default, the relative {@link #DFLT_SNAPSHOT_DIRECTORY} is used.
      * The value can be configured as relative path starting from the Ignites {@link #getWorkDirectory()}
      * or the value can be represented as an absolute snapshot working path instead.
      * @return {@code this} for chaining.
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/SystemDataRegionConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/SystemDataRegionConfiguration.java
new file mode 100644
index 0000000..bfa2004
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/SystemDataRegionConfiguration.java
@@ -0,0 +1,93 @@
+/*
+ * 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.configuration;
+
+import java.io.Serializable;
+import org.apache.ignite.internal.util.typedef.internal.A;
+
+/**
+ * This class allows defining system data region configuration with various parameters for Apache Ignite
+ * page memory (see {@link DataStorageConfiguration}.
+ * This class is similiar to {@link DataRegionConfiguration}, but with restricted set of properties.
+ */
+public class SystemDataRegionConfiguration implements Serializable {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** Default initial size in bytes of a memory chunk for the system cache (40 MB). */
+    public static final long DFLT_SYS_REG_INIT_SIZE = 40L * 1024 * 1024;
+
+    /** Default max size in bytes of a memory chunk for the system cache (100 MB). */
+    public static final long DFLT_SYS_REG_MAX_SIZE = 100L * 1024 * 1024;
+
+    /** Initial size in bytes of a memory chunk reserved for system cache. */
+    private long initSize = DFLT_SYS_REG_INIT_SIZE;
+
+    /** Maximum size in bytes of a memory chunk reserved for system cache. */
+    private long maxSize = DFLT_SYS_REG_MAX_SIZE;
+
+    /**
+     * Initial size of a data region reserved for system cache.
+     *
+     * @return Size in bytes.
+     */
+    public long getInitialSize() {
+        return initSize;
+    }
+
+    /**
+     * Sets initial size of a data region reserved for system cache.
+     *
+     * Default value is {@link #DFLT_SYS_REG_INIT_SIZE}
+     *
+     * @param initSize Size in bytes.
+     * @return {@code this} for chaining.
+     */
+    public SystemDataRegionConfiguration setInitialSize(long initSize) {
+        A.ensure(initSize > 0, "System region initial size should be greater than zero.");
+
+        this.initSize = initSize;
+
+        return this;
+    }
+
+    /**
+     * Maximum data region size in bytes reserved for system cache.
+     *
+     * @return Size in bytes.
+     */
+    public long getMaxSize() {
+        return maxSize;
+    }
+
+    /**
+     * Sets maximum data region size in bytes reserved for system cache. The total size should not be less than 10 MB
+     * due to internal data structures overhead.
+     *
+     * Default value is {@link #DFLT_SYS_REG_MAX_SIZE}.
+     *
+     * @param maxSize Maximum size in bytes for system cache data region.
+     * @return {@code this} for chaining.
+     */
+    public SystemDataRegionConfiguration setMaxSize(long maxSize) {
+        A.ensure(maxSize > 0, "System region max size should be greater than zero.");
+
+        this.maxSize = maxSize;
+
+        return this;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/events/CacheConsistencyViolationEvent.java b/modules/core/src/main/java/org/apache/ignite/events/CacheConsistencyViolationEvent.java
index 7512d8c..d02bf6e 100644
--- a/modules/core/src/main/java/org/apache/ignite/events/CacheConsistencyViolationEvent.java
+++ b/modules/core/src/main/java/org/apache/ignite/events/CacheConsistencyViolationEvent.java
@@ -19,6 +19,7 @@
 
 import java.util.Map;
 import org.apache.ignite.cache.CacheEntryVersion;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.lang.IgniteExperimental;
 
@@ -67,28 +68,39 @@
     private static final long serialVersionUID = 0L;
 
     /** Represents original values of entries.*/
-    final Map<Object, Map<ClusterNode, EntryInfo>> entries;
+    private final Map<Object, Map<ClusterNode, EntryInfo>> entries;
+
+    /** Fixed entries. */
+    private final Map<Object, Object> fixed;
 
     /** Cache name. */
-    final String cacheName;
+    private final String cacheName;
+
+    /** Strategy. */
+    private final ReadRepairStrategy strategy;
 
     /**
      * Creates a new instance of CacheConsistencyViolationEvent.
-     *
      * @param cacheName Cache name.
      * @param node Local node.
      * @param msg Event message.
      * @param entries Collection of original entries.
+     * @param fixed Collection of fixed entries.
+     * @param strategy Strategy.
      */
     public CacheConsistencyViolationEvent(
         String cacheName,
         ClusterNode node,
         String msg,
-        Map<Object, Map<ClusterNode, EntryInfo>> entries) {
+        Map<Object, Map<ClusterNode, EntryInfo>> entries,
+        Map<Object, Object> fixed,
+        ReadRepairStrategy strategy) {
         super(node, msg, EVT_CONSISTENCY_VIOLATION);
 
         this.cacheName = cacheName;
         this.entries = entries;
+        this.fixed = fixed;
+        this.strategy = strategy;
     }
 
     /**
@@ -101,6 +113,15 @@
     }
 
     /**
+     * Returns a mapping of keys to a collection of fixed entries.
+     *
+     * @return Collection of fixed entries.
+     */
+    public Map<Object, Object> getFixedEntries() {
+        return fixed;
+    }
+
+    /**
      * Returns cache name.
      *
      * @return Cache name.
@@ -110,6 +131,15 @@
     }
 
     /**
+     * Returns strategy.
+     *
+     * @return Strategy.
+     */
+    public ReadRepairStrategy getStrategy() {
+        return strategy;
+    }
+
+    /**
      * Inconsistent entry info.
      */
     public interface EntryInfo {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/GridJobResultImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/GridJobResultImpl.java
index 8fbce3f..860b44d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/GridJobResultImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/GridJobResultImpl.java
@@ -43,19 +43,19 @@
     /** */
     private ClusterNode node;
 
-    /** */
+    /** Guarded by {@code this}. */
     private Object data;
 
-    /** */
+    /** Guarded by {@code this}. */
     private IgniteException ex;
 
-    /** */
+    /** Guarded by {@code this}. */
     private boolean hasRes;
 
-    /** */
+    /** Guarded by {@code this}. */
     private boolean isCancelled;
 
-    /** */
+    /** Guarded by {@code this}. */
     private boolean isOccupied;
 
     /**
@@ -128,10 +128,12 @@
      * @param jobAttrs Job attributes.
      * @param isCancelled Whether job was cancelled or not.
      */
-    public synchronized void onResponse(@Nullable Object data,
+    public synchronized void onResponse(
+        @Nullable Object data,
         @Nullable IgniteException ex,
         @Nullable Map<Object, Object> jobAttrs,
-        boolean isCancelled) {
+        boolean isCancelled
+    ) {
         this.data = data;
         this.ex = ex;
         this.isCancelled = isCancelled;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/GridTaskSessionImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/GridTaskSessionImpl.java
index 45a0e94..25742b7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/GridTaskSessionImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/GridTaskSessionImpl.java
@@ -30,6 +30,7 @@
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.compute.ComputeJobSibling;
 import org.apache.ignite.compute.ComputeTaskSessionAttributeListener;
+import org.apache.ignite.compute.ComputeTaskSessionFullSupport;
 import org.apache.ignite.compute.ComputeTaskSessionScope;
 import org.apache.ignite.internal.managers.deployment.GridDeployment;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
@@ -43,6 +44,10 @@
 import org.apache.ignite.lang.IgniteUuid;
 import org.jetbrains.annotations.Nullable;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.unmodifiableCollection;
+
 /**
  * Task session.
  */
@@ -74,7 +79,7 @@
     /** */
     private Collection<ComputeJobSibling> siblings;
 
-    /** */
+    /** Guarded by {@link #mux}. */
     private Map<Object, Object> attrs;
 
     /** */
@@ -101,7 +106,7 @@
     /** */
     private final AtomicInteger usage = new AtomicInteger(1);
 
-    /** */
+    /** Task supports session attributes and checkpoints (there is {@link ComputeTaskSessionFullSupport}). */
     private final boolean fullSup;
 
     /** */
@@ -120,6 +125,17 @@
     private final String execName;
 
     /**
+     * Nodes on which the jobs of the task will be executed.
+     * Guarded by {@link #mux}.
+     */
+    @Nullable private List<UUID> jobNodes;
+
+    /** User who created the session, {@code null} if security is not enabled. */
+    @Nullable private final Object login;
+
+    /**
+     * Constructor.
+     *
      * @param taskNodeId Task node ID.
      * @param taskName Task name.
      * @param dep Deployment.
@@ -135,6 +151,7 @@
      * @param fullSup Session full support enabled flag.
      * @param internal Internal task flag.
      * @param execName Custom executor name.
+     * @param login User who created the session, {@code null} if security is not enabled.
      */
     public GridTaskSessionImpl(
         UUID taskNodeId,
@@ -151,7 +168,9 @@
         GridKernalContext ctx,
         boolean fullSup,
         boolean internal,
-        @Nullable String execName) {
+        @Nullable String execName,
+        @Nullable Object login
+    ) {
         assert taskNodeId != null;
         assert taskName != null;
         assert sesId != null;
@@ -169,7 +188,7 @@
         this.sesId = sesId;
         this.startTime = startTime;
         this.endTime = endTime;
-        this.siblings = siblings != null ? Collections.unmodifiableCollection(siblings) : null;
+        this.siblings = siblings != null ? unmodifiableCollection(siblings) : null;
         this.ctx = ctx;
 
         if (attrs != null && !attrs.isEmpty()) {
@@ -183,6 +202,8 @@
         this.execName = execName;
 
         mapFut = new IgniteFutureImpl(new GridFutureAdapter());
+
+        this.login = login;
     }
 
     /** {@inheritDoc} */
@@ -191,13 +212,17 @@
     }
 
     /**
+     * Checks that the task supports session attributes and checkpoints
+     * (there is {@link ComputeTaskSessionFullSupport}).
      *
+     * @throws IllegalStateException If not supported.
      */
     protected void checkFullSupport() {
-        if (!fullSup)
+        if (!fullSup) {
             throw new IllegalStateException("Sessions attributes and checkpoints are disabled by default " +
                 "for better performance (to enable, annotate task class with " +
-                "@ComputeTaskSessionFullSupport annotation).");
+                "@" + ComputeTaskSessionFullSupport.class.getSimpleName() + " annotation).");
+        }
     }
 
     /**
@@ -359,7 +384,7 @@
         checkFullSupport();
 
         if (keys.isEmpty())
-            return Collections.emptyMap();
+            return emptyMap();
 
         if (timeout == 0)
             timeout = Long.MAX_VALUE;
@@ -518,7 +543,7 @@
      */
     public void setJobSiblings(Collection<ComputeJobSibling> siblings) {
         synchronized (mux) {
-            this.siblings = Collections.unmodifiableCollection(siblings);
+            this.siblings = unmodifiableCollection(siblings);
         }
     }
 
@@ -534,7 +559,7 @@
             tmp.addAll(this.siblings);
             tmp.addAll(siblings);
 
-            this.siblings = Collections.unmodifiableCollection(tmp);
+            this.siblings = unmodifiableCollection(tmp);
         }
     }
 
@@ -606,7 +631,7 @@
         checkFullSupport();
 
         synchronized (mux) {
-            return attrs == null || attrs.isEmpty() ? Collections.emptyMap() : U.sealMap(attrs);
+            return attrs == null || attrs.isEmpty() ? emptyMap() : U.sealMap(attrs);
         }
     }
 
@@ -914,6 +939,42 @@
         return execName;
     }
 
+    /**
+     * Sets nodes on which the jobs of the task will be executed.
+     *
+     * @param jobNodes Nodes on which the jobs of the task will be executed.
+     */
+    public void jobNodes(Collection<UUID> jobNodes) {
+        synchronized (mux) {
+            this.jobNodes = F.isEmpty(jobNodes) ? emptyList() : new ArrayList<>(jobNodes);
+        }
+    }
+
+    /**
+     * @return Nodes on which the jobs of the task will be executed.
+     */
+    public List<UUID> jobNodesSafeCopy() {
+        synchronized (mux) {
+            return F.isEmpty(jobNodes) ? emptyList() : new ArrayList<>(jobNodes);
+        }
+    }
+
+    /**
+     * @return All session attributes, without checks.
+     */
+    public Map<Object, Object> attributesSafeCopy() {
+        synchronized (mux) {
+            return F.isEmpty(attrs) ? emptyMap() : new HashMap<>(attrs);
+        }
+    }
+
+    /**
+     * @return User who created the session, {@code null} if security is not enabled.
+     */
+    public Object login() {
+        return login;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return S.toString(GridTaskSessionImpl.class, this);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
index 9a15978..7c0f399 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
@@ -1037,6 +1037,7 @@
         ackP2pConfiguration();
         ackRebalanceConfiguration();
         ackIPv4StackFlagIsSet();
+        ackWaitForBackupsOnShutdownPropertyIsUsed();
 
         // Ack 3-rd party licenses location.
         if (log.isInfoEnabled() && cfg.getIgniteHome() != null)
@@ -2860,7 +2861,7 @@
             return;
 
         U.log(log, "System cache's DataRegion size is configured to " +
-            (memCfg.getSystemRegionInitialSize() / (1024 * 1024)) + " MB. " +
+            (memCfg.getSystemDataRegionConfiguration().getInitialSize() / (1024 * 1024)) + " MB. " +
             "Use DataStorageConfiguration.systemRegionInitialSize property to change the setting.");
     }
 
@@ -2983,6 +2984,16 @@
     }
 
     /**
+     * Prints warning if IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN is used.
+     */
+    private void ackWaitForBackupsOnShutdownPropertyIsUsed() {
+        if (IgniteSystemProperties.getString(IgniteSystemProperties.IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN) != null) {
+            log.warning("IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN system property is deprecated and will be removed " +
+                "in a future version. Use ShutdownPolicy instead.");
+        }
+    }
+
+    /**
      * @param cfg Ignite configuration to use.
      * @return Components provided in configuration which can implement {@link LifecycleAware} interface.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesEx.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesEx.java
index 0966a18..93be0af 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesEx.java
@@ -17,10 +17,10 @@
 
 package org.apache.ignite.internal;
 
+import java.util.Map;
 import java.util.function.Supplier;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteServices;
-import org.apache.ignite.services.ServiceCallContext;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -36,7 +36,7 @@
      * @param svcItf Interface for the service.
      * @param sticky Whether or not Ignite should always contact the same remote
      *      service or try to load-balance between services.
-     * @param callCtxProvider Caller context provider.
+     * @param callAttrsProvider Service call context attributes provider.
      * @param timeout If greater than 0 created proxy will wait for service availability only specified time,
      *  and will limit remote service invocation time.
      * @param <T> Service type.
@@ -48,7 +48,7 @@
         String name,
         Class<? super T> svcItf,
         boolean sticky,
-        @Nullable Supplier<ServiceCallContext> callCtxProvider,
+        @Nullable Supplier<Map<String, Object>> callAttrsProvider,
         long timeout
     ) throws IgniteException;
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesImpl.java
index 49277c8..f9a83ec 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteServicesImpl.java
@@ -24,18 +24,21 @@
 import java.io.ObjectStreamException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 import java.util.function.Supplier;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteServices;
 import org.apache.ignite.cluster.ClusterGroup;
 import org.apache.ignite.internal.cluster.ClusterGroupAdapter;
+import org.apache.ignite.internal.processors.service.ServiceCallContextImpl;
 import org.apache.ignite.internal.util.future.IgniteFutureImpl;
 import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallContextBuilder;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.jetbrains.annotations.Nullable;
@@ -47,6 +50,9 @@
     /** */
     private static final long serialVersionUID = 0L;
 
+    /** Default timeout. */
+    public static final long DFLT_TIMEOUT = 0;
+
     /** */
     private GridKernalContext ctx;
 
@@ -372,7 +378,7 @@
     /** {@inheritDoc} */
     @Override public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky)
         throws IgniteException {
-        return (T)serviceProxy(name, svcItf, sticky, 0);
+        return (T)serviceProxy(name, svcItf, sticky, DFLT_TIMEOUT);
     }
 
     /** {@inheritDoc} */
@@ -382,7 +388,18 @@
         final boolean sticky,
         final long timeout
     ) throws IgniteException {
-        return (T)serviceProxy(name, svcItf, sticky, (Supplier<ServiceCallContext>)null, timeout);
+        return (T)serviceProxy(name, svcItf, sticky, (Supplier<Map<String, Object>>)null, timeout);
+    }
+
+    /** */
+    public <T> T serviceProxy(
+        final String name,
+        final Class<? super T> svcItf,
+        final boolean sticky,
+        final long timeout,
+        final boolean keepBinary
+    ) throws IgniteException {
+        return (T)serviceProxy(name, svcItf, sticky, null, timeout, keepBinary);
     }
 
     /** {@inheritDoc} */
@@ -392,7 +409,7 @@
         final boolean sticky,
         @Nullable ServiceCallContext callCtx
     ) throws IgniteException {
-        return (T)serviceProxy(name, svcItf, sticky, callCtx, 0);
+        return (T)serviceProxy(name, svcItf, sticky, callCtx, DFLT_TIMEOUT);
     }
 
     /** {@inheritDoc} */
@@ -403,7 +420,11 @@
         @Nullable ServiceCallContext callCtx,
         final long timeout
     ) throws IgniteException {
-        return (T)serviceProxy(name, svcItf, sticky, callCtx != null ? () -> callCtx : null, timeout);
+        A.ensure(callCtx == null || callCtx instanceof ServiceCallContextImpl,
+            "\"callCtx\" has an invalid type. Custom implementation of " + ServiceCallContext.class.getSimpleName() +
+                " is not supported. Please use " + ServiceCallContextBuilder.class.getSimpleName() + " to create it.");
+
+        return (T)serviceProxy(name, svcItf, sticky, callCtx != null ? ((ServiceCallContextImpl)callCtx)::values : null, timeout);
     }
 
     /** {@inheritDoc} */
@@ -411,9 +432,21 @@
         final String name,
         final Class<? super T> svcItf,
         final boolean sticky,
-        @Nullable Supplier<ServiceCallContext> callCtxProvider,
+        @Nullable Supplier<Map<String, Object>> callAttrsProvider,
         final long timeout
     ) throws IgniteException {
+        return serviceProxy(name, svcItf, sticky, callAttrsProvider, timeout, false);
+    }
+
+    /** */
+    private <T> T serviceProxy(
+        final String name,
+        final Class<? super T> svcItf,
+        final boolean sticky,
+        @Nullable Supplier<Map<String, Object>> callAttrsProvider,
+        final long timeout,
+        final boolean keepBinary
+    ) {
         A.notNull(name, "name");
         A.notNull(svcItf, "svcItf");
         A.ensure(svcItf.isInterface(), "Service class must be an interface: " + svcItf);
@@ -422,7 +455,7 @@
         guard();
 
         try {
-            return (T)ctx.service().serviceProxy(prj, name, svcItf, sticky, callCtxProvider, timeout);
+            return (T)ctx.service().serviceProxy(prj, name, svcItf, sticky, callAttrsProvider, timeout, keepBinary);
         }
         finally {
             unguard();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java b/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
index b6d4fe0..a84715a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
@@ -69,9 +69,11 @@
 import org.apache.ignite.configuration.MemoryConfiguration;
 import org.apache.ignite.configuration.MemoryPolicyConfiguration;
 import org.apache.ignite.configuration.PersistentStoreConfiguration;
+import org.apache.ignite.configuration.SystemDataRegionConfiguration;
 import org.apache.ignite.configuration.TransactionConfiguration;
 import org.apache.ignite.failure.FailureContext;
 import org.apache.ignite.failure.FailureType;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.managers.discovery.GridDiscoveryManager;
 import org.apache.ignite.internal.processors.cache.CacheGroupContext;
@@ -1871,6 +1873,8 @@
          */
         private IgniteConfiguration initializeConfiguration(IgniteConfiguration cfg)
             throws IgniteCheckedException {
+            BinaryArray.initUseBinaryArrays();
+
             IgniteConfiguration myCfg = new IgniteConfiguration(cfg);
 
             String ggHome = cfg.getIgniteHome();
@@ -2695,8 +2699,12 @@
 
         dsCfg.setConcurrencyLevel(memCfg.getConcurrencyLevel());
         dsCfg.setPageSize(memCfg.getPageSize());
-        dsCfg.setSystemRegionInitialSize(memCfg.getSystemCacheInitialSize());
-        dsCfg.setSystemRegionMaxSize(memCfg.getSystemCacheMaxSize());
+
+        dsCfg.setSystemDataRegionConfiguration(
+                new SystemDataRegionConfiguration()
+                        .setInitialSize(memCfg.getSystemCacheInitialSize())
+                        .setMaxSize(memCfg.getSystemCacheMaxSize())
+        );
 
         List<DataRegionConfiguration> optionalDataRegions = new ArrayList<>();
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryArray.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryArray.java
new file mode 100644
index 0000000..fdecc30
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryArray.java
@@ -0,0 +1,286 @@
+/*
+ * 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.binary;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.internal.GridDirectTransient;
+import org.apache.ignite.internal.processors.cache.CacheObjectUtils;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.tostring.GridToStringExclude;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+import static org.apache.ignite.internal.binary.GridBinaryMarshaller.UNREGISTERED_TYPE_ID;
+
+/**
+ * Binary object representing array.
+ */
+public class BinaryArray implements BinaryObjectEx, Externalizable, Comparable<BinaryArray> {
+    /** Default value of {@link IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS}. */
+    public static final boolean DFLT_IGNITE_USE_BINARY_ARRAYS = false;
+
+    /** Value of {@link IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS}. */
+    private static boolean USE_BINARY_ARRAYS =
+        IgniteSystemProperties.getBoolean(IGNITE_USE_BINARY_ARRAYS, DFLT_IGNITE_USE_BINARY_ARRAYS);
+
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Context. */
+    @GridDirectTransient
+    @GridToStringExclude
+    protected BinaryContext ctx;
+
+    /** Type ID. */
+    protected int compTypeId;
+
+    /** Type class name. */
+    @Nullable protected String compClsName;
+
+    /** Values. */
+    @GridToStringInclude(sensitive = true)
+    protected Object[] arr;
+
+    /** Deserialized value. */
+    @GridToStringExclude
+    protected Object[] deserialized;
+
+    /**
+     * {@link Externalizable} support.
+     */
+    public BinaryArray() {
+        // No-op.
+    }
+
+    /**
+     * @param ctx Context.
+     * @param compTypeId Component type id.
+     * @param compClsName Component class name.
+     * @param arr Array.
+     */
+    public BinaryArray(BinaryContext ctx, int compTypeId, @Nullable String compClsName, Object[] arr) {
+        this.ctx = ctx;
+        this.compTypeId = compTypeId;
+        this.compClsName = compClsName;
+        this.arr = arr;
+    }
+
+    /** {@inheritDoc} */
+    @Override public BinaryType type() throws BinaryObjectException {
+        return BinaryUtils.typeProxy(ctx, this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable BinaryType rawType() throws BinaryObjectException {
+        return BinaryUtils.type(ctx, this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T deserialize() throws BinaryObjectException {
+        return (T)deserialize(null);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T deserialize(ClassLoader ldr) throws BinaryObjectException {
+        ClassLoader resolveLdr = ldr == null ? ctx.configuration().getClassLoader() : ldr;
+
+        if (ldr != null)
+            GridBinaryMarshaller.USE_CACHE.set(Boolean.FALSE);
+
+        try {
+            Class<?> compType = BinaryUtils.resolveClass(ctx, compTypeId, compClsName, resolveLdr, false);
+
+            // Skip deserialization if already deserialized.
+            // Prepared result is in arr, already.
+            if (deserialized != null)
+                return (T)deserialized;
+
+            deserialized = (Object[])Array.newInstance(compType, arr.length);
+
+            for (int i = 0; i < arr.length; i++) {
+                Object obj = CacheObjectUtils.unwrapBinaryIfNeeded(null, arr[i], false, false, ldr);
+
+                if (obj instanceof BinaryObject)
+                    obj = ((BinaryObject)obj).deserialize(ldr);
+
+                deserialized[i] = obj;
+            }
+
+            return (T)deserialized;
+        }
+        finally {
+            GridBinaryMarshaller.USE_CACHE.set(Boolean.TRUE);
+        }
+    }
+
+    /**
+     * @return Underlying array.
+     */
+    public Object[] array() {
+        return arr;
+    }
+
+    /**
+     * @return Component type ID.
+     */
+    public int componentTypeId() {
+        // This can happen when binary type was not registered in time of binary array creation.
+        // In this case same type will be written differently:
+        // arr1 = [compTypeId=UNREGISTERED_TYPE_ID,compClsName="org.apache.Pojo"]
+        // arr2 = [comTypeId=1234,compClsName=null]
+        // Overcome by calculation compTypeId based on compClsName.
+        return compTypeId == UNREGISTERED_TYPE_ID ? ctx.typeId(compClsName) : compTypeId;
+    }
+
+    /**
+     * @return Component class name.
+     */
+    public String componentClassName() {
+        return compClsName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public BinaryObject clone() throws CloneNotSupportedException {
+        return new BinaryArray(ctx, compTypeId, compClsName, arr.clone());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int typeId() {
+        return GridBinaryMarshaller.OBJ_ARR;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeInt(compTypeId);
+        out.writeObject(compClsName);
+        out.writeObject(arr);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        ctx = GridBinaryMarshaller.threadLocalContext();
+
+        compTypeId = in.readInt();
+        compClsName = (String)in.readObject();
+        arr = (Object[])in.readObject();
+    }
+
+    /** {@inheritDoc} */
+    @Override public BinaryObjectBuilder toBuilder() throws BinaryObjectException {
+        throw new UnsupportedOperationException("Builder cannot be created for array wrapper.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public int enumOrdinal() throws BinaryObjectException {
+        throw new BinaryObjectException("Object is not enum.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public String enumName() throws BinaryObjectException {
+        throw new BinaryObjectException("Object is not enum.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public int size() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isFlagSet(short flag) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <F> F field(String fieldName) throws BinaryObjectException {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean hasField(String fieldName) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int result = 31 * Objects.hash(componentTypeId());
+
+        result = 31 * result + IgniteUtils.hashCode(arr);
+
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        BinaryArray arr = (BinaryArray)o;
+
+        return componentTypeId() == arr.componentTypeId()
+            && Arrays.deepEquals(this.arr, arr.arr);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int compareTo(@NotNull BinaryArray o) {
+        if (componentTypeId() != o.componentTypeId()) {
+            throw new IllegalArgumentException(
+                "Can't compare arrays of different types[this=" + componentTypeId() + ",that=" + o.componentTypeId() + ']'
+            );
+        }
+
+        return F.compareArrays(arr, o.arr);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(BinaryArray.class, this);
+    }
+
+    /** @return {@code True} if typed arrays should be used, {@code false} otherwise. */
+    public static boolean useBinaryArrays() {
+        return USE_BINARY_ARRAYS;
+    }
+
+    /**
+     * Initialize {@link #USE_BINARY_ARRAYS} value with
+     * {@link IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS} system property value.
+     *
+     * This method invoked using reflection in tests.
+     */
+    public static void initUseBinaryArrays() {
+        USE_BINARY_ARRAYS = IgniteSystemProperties.getBoolean(IGNITE_USE_BINARY_ARRAYS, DFLT_IGNITE_USE_BINARY_ARRAYS);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryClassDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryClassDescriptor.java
index f15ac5b..6ded172 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryClassDescriptor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryClassDescriptor.java
@@ -756,7 +756,10 @@
                     break;
 
                 case OBJECT_ARR:
-                    writer.doWriteObjectArray((Object[])obj);
+                    if (obj instanceof BinaryArray)
+                        writer.doWriteBinaryArray(((BinaryArray)obj));
+                    else
+                        writer.doWriteObjectArray((Object[])obj);
 
                     break;
 
@@ -781,7 +784,10 @@
                     break;
 
                 case ENUM_ARR:
-                    writer.doWriteEnumArray((Object[])obj);
+                    if (obj instanceof BinaryArray)
+                        writer.doWriteBinaryArray(((BinaryArray)obj));
+                    else
+                        writer.doWriteEnumArray((Object[])obj);
 
                     break;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java
index bb251180..ff9407f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java
@@ -268,6 +268,8 @@
         registerPredefinedType(BinaryMetadata.class, 0);
         registerPredefinedType(BinaryEnumObjectImpl.class, 0);
         registerPredefinedType(BinaryTreeMap.class, 0);
+        registerPredefinedType(BinaryArray.class, 0);
+        registerPredefinedType(BinaryEnumArray.class, 0);
 
         registerPredefinedType(PlatformDotNetSessionData.class, 0);
         registerPredefinedType(PlatformDotNetSessionLockResult.class, 0);
@@ -601,7 +603,7 @@
      * @return A descriptor for the given class. If the class hasn't been registered yet, then a new descriptor will be
      * created, but its {@link BinaryClassDescriptor#registered()} will be {@code false}.
      */
-    @NotNull BinaryClassDescriptor descriptorForClass(Class<?> cls) {
+    @NotNull public BinaryClassDescriptor descriptorForClass(Class<?> cls) {
         assert cls != null;
 
         BinaryClassDescriptor desc = descByCls.get(cls);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryEnumArray.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryEnumArray.java
new file mode 100644
index 0000000..f942f79
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryEnumArray.java
@@ -0,0 +1,64 @@
+/*
+ * 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.binary;
+
+import java.io.Externalizable;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Ignite distinguishes between array of objects and array of enums.
+ * This extension of {@link BinaryArray} intended to keep correct typeId for binary enum arrays.
+ */
+public class BinaryEnumArray extends BinaryArray {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /**
+     * {@link Externalizable} support.
+     */
+    public BinaryEnumArray() {
+    }
+
+    /**
+     * @param ctx Context.
+     * @param compTypeId Component type id.
+     * @param compClsName Component class name.
+     * @param arr Array.
+     */
+    public BinaryEnumArray(BinaryContext ctx, int compTypeId,
+        @Nullable String compClsName, Object[] arr) {
+        super(ctx, compTypeId, compClsName, arr);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int typeId() {
+        return GridBinaryMarshaller.ENUM_ARR;
+    }
+
+    /** {@inheritDoc} */
+    @Override public BinaryObject clone() throws CloneNotSupportedException {
+        return new BinaryEnumArray(ctx, compTypeId, compClsName, arr.clone());
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(BinaryEnumArray.class, this, super.toString());
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryMarshaller.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryMarshaller.java
index ca5f8f0..7db76c1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryMarshaller.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryMarshaller.java
@@ -73,7 +73,7 @@
      * <p/>
      * @param ctx Binary context.
      */
-    private void setBinaryContext(BinaryContext ctx, IgniteConfiguration cfg) {
+    public void setBinaryContext(BinaryContext ctx, IgniteConfiguration cfg) {
         ctx.configure(this, cfg != null ? cfg.getBinaryConfiguration() : null);
 
         impl = new GridBinaryMarshaller(ctx);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
index 19b04db..acedfc5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
@@ -41,6 +41,7 @@
 import org.apache.ignite.internal.processors.cache.CacheObjectValueContext;
 import org.apache.ignite.internal.processors.cache.KeyCacheObject;
 import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.plugin.extensions.communication.MessageReader;
 import org.apache.ignite.plugin.extensions.communication.MessageWriter;
@@ -77,10 +78,6 @@
     /** */
     private int part = -1;
 
-    /** */
-    @GridDirectTransient
-    private BinaryReaderHandles handles;
-
     /**
      * For {@link Externalizable}.
      */
@@ -100,8 +97,6 @@
         this.ctx = ctx;
         this.arr = arr;
         this.start = start;
-
-        handles = new BinaryReaderHandles();
     }
 
     /** {@inheritDoc} */
@@ -322,12 +317,12 @@
 
     /** {@inheritDoc} */
     @Nullable @Override public <F> F field(String fieldName) throws BinaryObjectException {
-        return (F)reader(handles, false).unmarshalField(fieldName);
+        return (F)reader(null, false).unmarshalField(fieldName);
     }
 
     /** {@inheritDoc} */
     @Nullable @Override public <F> F field(int fieldId) throws BinaryObjectException {
-        return (F)reader(handles, false).unmarshalField(fieldId);
+        return (F)reader(null, false).unmarshalField(fieldId);
     }
 
     /** {@inheritDoc} */
@@ -896,8 +891,12 @@
         else {
             if (secondBinary)
                 return -1; // Go to the left part.
-            else
+            else {
+                if (F.isArray(first) && F.isArray(second))
+                    return F.compareArrays(first, second);
+
                 return ((Comparable)first).compareTo(second);
+            }
         }
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryReaderExImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryReaderExImpl.java
index 6327589..a88ed79 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryReaderExImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryReaderExImpl.java
@@ -1367,7 +1367,12 @@
                 return BinaryUtils.doReadObjectArray(in, ctx, ldr, this, false, true);
 
             case HANDLE:
-                return readHandleField();
+                Object arr = readHandleField();
+
+                if (arr instanceof BinaryArray)
+                    return ((BinaryArray)arr).deserialize(ldr);
+                else
+                    return (Object[])arr;
 
             default:
                 return null;
@@ -1475,7 +1480,12 @@
                 return BinaryUtils.doReadEnumArray(in, ctx, ldr, cls);
 
             case HANDLE:
-                return readHandleField();
+                Object arr = readHandleField();
+
+                if (arr instanceof BinaryArray)
+                    return ((BinaryArray)arr).deserialize(ldr);
+                else
+                    return (Object[])arr;
 
             default:
                 return null;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinarySerializedFieldComparator.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinarySerializedFieldComparator.java
index d1e7969..03b0a0d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinarySerializedFieldComparator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinarySerializedFieldComparator.java
@@ -17,10 +17,8 @@
 
 package org.apache.ignite.internal.binary;
 
-import java.util.Arrays;
 import org.apache.ignite.internal.util.offheap.unsafe.GridUnsafeMemory;
 import org.apache.ignite.internal.util.typedef.F;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Compares fiels in serialized form when possible.
@@ -247,51 +245,11 @@
                 Object val1 = c1.currentField();
                 Object val2 = c2.currentField();
 
-                return isArray(val1) ? compareArrays(val1, val2) : F.eq(val1, val2);
+                return (F.isArray(val1) || val1 instanceof BinaryArray) ? F.arrayEq(val1, val2) : F.eq(val1, val2);
         }
     }
 
     /**
-     * Compare arrays.
-     *
-     * @param val1 Value 1.
-     * @param val2 Value 2.
-     * @return Result.
-     */
-    private static boolean compareArrays(Object val1, Object val2) {
-        if (val1.getClass() == val2.getClass()) {
-            if (val1 instanceof byte[])
-                return Arrays.equals((byte[])val1, (byte[])val2);
-            else if (val1 instanceof boolean[])
-                return Arrays.equals((boolean[])val1, (boolean[])val2);
-            else if (val1 instanceof short[])
-                return Arrays.equals((short[])val1, (short[])val2);
-            else if (val1 instanceof char[])
-                return Arrays.equals((char[])val1, (char[])val2);
-            else if (val1 instanceof int[])
-                return Arrays.equals((int[])val1, (int[])val2);
-            else if (val1 instanceof long[])
-                return Arrays.equals((long[])val1, (long[])val2);
-            else if (val1 instanceof float[])
-                return Arrays.equals((float[])val1, (float[])val2);
-            else if (val1 instanceof double[])
-                return Arrays.equals((double[])val1, (double[])val2);
-            else
-                return Arrays.deepEquals((Object[])val1, (Object[])val2);
-        }
-
-        return false;
-    }
-
-    /**
-     * @param field Field.
-     * @return {@code True} if field is array.
-     */
-    private static boolean isArray(@Nullable Object field) {
-        return field != null && field.getClass().isArray();
-    }
-
-    /**
      * Compare byte arrays.
      *
      * @param c1 Comparer 1.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryUtils.java
index 1f51af2..dafb825 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryUtils.java
@@ -610,6 +610,12 @@
             return cls.getComponentType().isEnum() || cls.getComponentType() == Enum.class ?
                 GridBinaryMarshaller.ENUM_ARR : GridBinaryMarshaller.OBJ_ARR;
 
+        if (cls == BinaryArray.class)
+            return GridBinaryMarshaller.OBJ_ARR;
+
+        if (cls == BinaryEnumArray.class)
+            return GridBinaryMarshaller.ENUM_ARR;
+
         if (isSpecialCollection(cls))
             return GridBinaryMarshaller.COL;
 
@@ -1161,6 +1167,10 @@
             return BinaryWriteMode.TIME_ARR;
         else if (cls.isArray())
             return cls.getComponentType().isEnum() ? BinaryWriteMode.ENUM_ARR : BinaryWriteMode.OBJECT_ARR;
+        else if (cls == BinaryArray.class)
+            return BinaryWriteMode.OBJECT_ARR;
+        else if (cls == BinaryEnumArray.class)
+            return BinaryWriteMode.ENUM_ARR;
         else if (cls == BinaryObjectImpl.class)
             return BinaryWriteMode.BINARY_OBJ;
         else if (Binarylizable.class.isAssignableFrom(cls))
@@ -1892,11 +1902,6 @@
             }
 
             case GridBinaryMarshaller.OBJ: {
-                Object obj = handles.getHandle(start);
-
-                if (obj != null)
-                    return obj;
-
                 checkProtocolVersion(in.readByte());
 
                 int len = length(in, start);
@@ -2009,7 +2014,10 @@
                 return doReadTimeArray(in);
 
             case GridBinaryMarshaller.OBJ_ARR:
-                return doReadObjectArray(in, ctx, ldr, handles, detach, deserialize);
+                if (BinaryArray.useBinaryArrays() && !deserialize)
+                    return doReadBinaryArray(in, ctx, ldr, handles, detach, deserialize, false);
+                else
+                    return doReadObjectArray(in, ctx, ldr, handles, detach, deserialize);
 
             case GridBinaryMarshaller.COL:
                 return doReadCollection(in, ctx, ldr, handles, detach, deserialize, null);
@@ -2025,9 +2033,13 @@
                 return doReadBinaryEnum(in, ctx, doReadEnumType(in));
 
             case GridBinaryMarshaller.ENUM_ARR:
-                doReadEnumType(in); // Simply skip this part as we do not need it.
+                if (BinaryArray.useBinaryArrays() && !deserialize)
+                    return doReadBinaryArray(in, ctx, ldr, handles, detach, deserialize, true);
+                else {
+                    doReadEnumType(in); // Simply skip this part as we do not need it.
 
-                return doReadBinaryEnumArray(in, ctx);
+                    return doReadBinaryEnumArray(in, ctx);
+                }
 
             case GridBinaryMarshaller.CLASS:
                 return doReadClass(in, ctx, ldr);
@@ -2061,14 +2073,58 @@
 
         int len = in.readInt();
 
-        Object[] arr = deserialize ? (Object[])Array.newInstance(compType, len) : new Object[len];
+        Object[] arr = (deserialize && !BinaryObject.class.isAssignableFrom(compType))
+            ? (Object[])Array.newInstance(compType, len)
+            : new Object[len];
 
         handles.setHandle(arr, hPos);
 
+        for (int i = 0; i < len; i++) {
+            Object res = deserializeOrUnmarshal(in, ctx, ldr, handles, detach, deserialize);
+
+            if (deserialize && BinaryArray.useBinaryArrays() && res instanceof BinaryObject)
+                arr[i] = ((BinaryObject)res).deserialize(ldr);
+            else
+                arr[i] = res;
+        }
+
+        return arr;
+    }
+
+    /**
+     * @param in Binary input stream.
+     * @param ctx Binary context.
+     * @param ldr Class loader.
+     * @param handles Holder for handles.
+     * @param detach Detach flag.
+     * @param deserialize Deep flag.
+     * @return Value.
+     * @throws BinaryObjectException In case of error.
+     */
+    public static BinaryArray doReadBinaryArray(BinaryInputStream in, BinaryContext ctx, ClassLoader ldr,
+        BinaryReaderHandlesHolder handles, boolean detach, boolean deserialize, boolean isEnumArray) {
+        int hPos = positionForHandle(in);
+
+        int compTypeId = in.readInt();
+        String compClsName = null;
+
+        if (compTypeId == GridBinaryMarshaller.UNREGISTERED_TYPE_ID)
+            compClsName = doReadClassName(in);
+
+        int len = in.readInt();
+
+        Object[] arr = new Object[len];
+
+        BinaryArray res = isEnumArray
+            ? new BinaryEnumArray(ctx, compTypeId, compClsName, arr)
+            : new BinaryArray(ctx, compTypeId, compClsName, arr);
+
+        handles.setHandle(res, hPos);
+
         for (int i = 0; i < len; i++)
             arr[i] = deserializeOrUnmarshal(in, ctx, ldr, handles, detach, deserialize);
 
-        return arr;
+        return res;
     }
 
     /**
@@ -2083,11 +2139,6 @@
         throws BinaryObjectException {
         int hPos = positionForHandle(in);
 
-        Object obj = handles.getHandle(hPos);
-
-        if (obj != null)
-            return (Collection<?>)obj;
-
         int size = in.readInt();
 
         assert size >= 0;
@@ -2160,11 +2211,6 @@
         throws BinaryObjectException {
         int hPos = positionForHandle(in);
 
-        Object obj = handles.getHandle(hPos);
-
-        if (obj != null)
-            return (Map<?, ?>)obj;
-
         int size = in.readInt();
 
         assert size >= 0;
@@ -2570,6 +2616,25 @@
     }
 
     /**
+     * @param obj {@link BinaryArray} or {@code Object[]}.
+     * @return Objects array.
+     */
+    public static Object[] rawArrayFromBinary(Object obj) {
+        if (obj instanceof BinaryArray)
+            // We want raw data(no deserialization).
+            return ((BinaryArray)obj).array();
+        else
+            // This can happen even in BinaryArray.USE_TYPED_ARRAY = true.
+            // In case user pass special array type to arguments, String[], for example.
+            return (Object[])obj;
+    }
+
+    /** */
+    public static boolean isObjectArray(Class<?> cls) {
+        return Object[].class == cls || BinaryArray.class == cls || BinaryEnumArray.class == cls;
+    }
+
+    /**
      * Enum type.
      */
     private static class EnumType {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java
index 7343e95..f3ccd0b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java
@@ -776,6 +776,34 @@
     }
 
     /**
+     * @param val Array wrapper.
+     * @throws BinaryObjectException In case of error.
+     */
+    void doWriteBinaryArray(BinaryArray val) throws BinaryObjectException {
+        if (val.array() == null)
+            out.writeByte(GridBinaryMarshaller.NULL);
+        else {
+            if (tryWriteAsHandle(val))
+                return;
+
+            out.unsafeEnsure(1 + 4);
+            out.unsafeWriteByte(val instanceof BinaryEnumArray
+                ? GridBinaryMarshaller.ENUM_ARR
+                : GridBinaryMarshaller.OBJ_ARR
+            );
+            out.unsafeWriteInt(val.componentTypeId());
+
+            if (val.componentTypeId() == GridBinaryMarshaller.UNREGISTERED_TYPE_ID)
+                doWriteString(val.componentClassName());
+
+            out.writeInt(val.array().length);
+
+            for (Object obj : val.array())
+                doWriteObject(obj);
+        }
+    }
+
+    /**
      * @param col Collection.
      * @throws org.apache.ignite.binary.BinaryObjectException In case of error.
      */
@@ -1902,7 +1930,7 @@
      * @param obj Object to write.
      * @return {@code true} if the object has been written as a handle.
      */
-    public boolean tryWriteAsHandle(Object obj) {
+    boolean tryWriteAsHandle(Object obj) {
         assert obj != null;
 
         int pos = out.position();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryBuilderSerializer.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryBuilderSerializer.java
index 7764699..ccd3ac2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryBuilderSerializer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryBuilderSerializer.java
@@ -21,6 +21,8 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.binary.BinaryArray;
+import org.apache.ignite.internal.binary.BinaryEnumArray;
 import org.apache.ignite.internal.binary.BinaryEnumObjectImpl;
 import org.apache.ignite.internal.binary.BinaryObjectExImpl;
 import org.apache.ignite.internal.binary.BinaryUtils;
@@ -70,9 +72,6 @@
         }
 
         if (val instanceof BinaryBuilderSerializationAware) {
-            if (writer.tryWriteAsHandle(val))
-                return;
-            
             ((BinaryBuilderSerializationAware)val).writeTo(writer, this);
 
             return;
@@ -145,9 +144,6 @@
         if (forceCol || BinaryUtils.isSpecialCollection(val.getClass())) {
             Collection<?> c = (Collection<?>)val;
 
-            if (writer.tryWriteAsHandle(c))
-                return;
-            
             writer.writeByte(GridBinaryMarshaller.COL);
             writer.writeInt(c.size());
 
@@ -164,9 +160,6 @@
         if (forceMap || BinaryUtils.isSpecialMap(val.getClass())) {
             Map<?, ?> map = (Map<?, ?>)val;
 
-            if (writer.tryWriteAsHandle(map))
-                return;
-
             writer.writeByte(GridBinaryMarshaller.MAP);
             writer.writeInt(map.size());
 
@@ -188,6 +181,28 @@
             return;
         }
 
+        if (val instanceof BinaryEnumArray) {
+            BinaryArray val0 = (BinaryArray)val;
+
+            if (val0.componentTypeId() == GridBinaryMarshaller.UNREGISTERED_TYPE_ID)
+                writeArray(writer, GridBinaryMarshaller.ENUM_ARR, val0.array(), val0.componentClassName());
+            else
+                writeArray(writer, GridBinaryMarshaller.ENUM_ARR, val0.array(), val0.componentTypeId());
+
+            return;
+        }
+
+        if (val instanceof BinaryArray) {
+            BinaryArray val0 = (BinaryArray)val;
+
+            if (val0.componentTypeId() == GridBinaryMarshaller.UNREGISTERED_TYPE_ID)
+                writeArray(writer, GridBinaryMarshaller.OBJ_ARR, val0.array(), val0.componentClassName());
+            else
+                writeArray(writer, GridBinaryMarshaller.OBJ_ARR, val0.array(), val0.componentTypeId());
+
+            return;
+        }
+
         if (val instanceof Object[]) {
             Class<?> compCls = ((Object[])val).getClass().getComponentType();
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java
index 6ca4047..c1f9e60 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/builder/BinaryObjectBuilderImpl.java
@@ -29,7 +29,9 @@
 import org.apache.ignite.binary.BinaryObjectBuilder;
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryContext;
+import org.apache.ignite.internal.binary.BinaryEnumArray;
 import org.apache.ignite.internal.binary.BinaryEnumObjectImpl;
 import org.apache.ignite.internal.binary.BinaryFieldMetadata;
 import org.apache.ignite.internal.binary.BinaryMetadata;
@@ -411,6 +413,12 @@
         else if (newVal.getClass().isArray() && BinaryObject.class.isAssignableFrom(newVal.getClass().getComponentType()))
             newFldTypeId = GridBinaryMarshaller.OBJ_ARR;
 
+        else if (newVal instanceof BinaryEnumArray)
+            newFldTypeId = GridBinaryMarshaller.ENUM_ARR;
+
+        else if (newVal instanceof BinaryArray)
+            newFldTypeId = GridBinaryMarshaller.OBJ_ARR;
+
         else
             newFldTypeId = BinaryUtils.typeByClass(newVal.getClass());
 
@@ -541,13 +549,15 @@
             if (val == REMOVED_FIELD_MARKER)
                 return null;
         }
-        else {
+        else if (reader != null) {
             ensureReadCacheInit();
 
             int fldId = ctx.fieldId(typeId, name);
 
             val = readCache.get(fldId);
         }
+        else
+            return null;
 
         return (T)BinaryUtils.unwrapLazy(val);
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/streams/BinaryHeapOutputStream.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/streams/BinaryHeapOutputStream.java
index 4a027e2..325cce2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/streams/BinaryHeapOutputStream.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/streams/BinaryHeapOutputStream.java
@@ -28,6 +28,9 @@
     /** Allocator. */
     private final BinaryMemoryAllocatorChunk chunk;
 
+    /** Disable auto close flag. */
+    private final boolean disableAutoClose;
+
     /** Data. */
     private byte[] data;
 
@@ -47,13 +50,34 @@
      * @param chunk Chunk.
      */
     public BinaryHeapOutputStream(int cap, BinaryMemoryAllocatorChunk chunk) {
+        this(cap, chunk, false);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param cap Capacity.
+     * @param chunk Chunk.
+     * @param disableAutoClose Whether to disable resource release in {@link BinaryHeapOutputStream#close()} method
+     *                         so that an explicit {@link BinaryHeapOutputStream#release()} call is required.
+     */
+    public BinaryHeapOutputStream(int cap, BinaryMemoryAllocatorChunk chunk, boolean disableAutoClose) {
         this.chunk = chunk;
+        this.disableAutoClose = disableAutoClose;
 
         data = chunk.allocate(cap);
     }
 
     /** {@inheritDoc} */
     @Override public void close() {
+        if (!disableAutoClose)
+            release();
+    }
+
+    /**
+     * Releases pooled memory.
+     */
+    public void release() {
         chunk.release(data, pos);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
index e0abac1..89dbe4b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
@@ -23,6 +23,7 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
@@ -43,12 +44,16 @@
 import org.apache.ignite.internal.cache.query.index.sorted.SortedSegmentedIndex;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexImpl;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexTree;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKeyFactory;
 import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.cache.CacheObjectUtils;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
+import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
 import org.apache.ignite.internal.processors.cache.query.IndexQueryDesc;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.GridCloseableIteratorAdapter;
@@ -57,9 +62,12 @@
 import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteBiTuple;
+import org.apache.ignite.spi.indexing.IndexingQueryFilter;
 import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.internal.cache.query.index.SortOrder.DESC;
+import static org.apache.ignite.internal.cache.query.index.sorted.inline.types.NullableInlineIndexKeyType.CANT_BE_COMPARE;
+import static org.apache.ignite.internal.cache.query.index.sorted.inline.types.NullableInlineIndexKeyType.COMPARE_UNSUPPORTED;
 
 /**
  * Processor of {@link IndexQuery}.
@@ -82,14 +90,14 @@
         GridCacheContext<K, V> cctx,
         IndexQueryDesc idxQryDesc,
         @Nullable IgniteBiPredicate<K, V> filter,
-        IndexQueryContext qryCtx,
+        IndexingQueryFilter cacheFilter,
         boolean keepBinary
     ) throws IgniteCheckedException {
         SortedSegmentedIndex idx = findSortedIndex(cctx, idxQryDesc);
 
         IndexRangeQuery qry = prepareQuery(idx, idxQryDesc);
 
-        GridCursor<IndexRow> cursor = querySortedIndex(cctx, idx, qryCtx, qry);
+        GridCursor<IndexRow> cursor = querySortedIndex(cctx, idx, cacheFilter, qry);
 
         SortedIndexDefinition def = (SortedIndexDefinition)idxProc.indexDefinition(idx.id());
 
@@ -477,11 +485,24 @@
     private GridCursor<IndexRow> querySortedIndex(
         GridCacheContext<?, ?> cctx,
         SortedSegmentedIndex idx,
-        IndexQueryContext qryCtx,
+        IndexingQueryFilter cacheFilter,
         IndexRangeQuery qry
     ) throws IgniteCheckedException {
         int segmentsCnt = cctx.isPartitioned() ? cctx.config().getQueryParallelism() : 1;
 
+        BPlusTree.TreeRowClosure<IndexRow, IndexRow> treeFilter = null;
+
+        // No need in the additional filter step for queries with 0 or 1 criteria.
+        // Also skips filtering if the current search is unbounded (both boundaries equal to null).
+        if (qry.criteria.length > 1 && !(qry.lower == null && qry.upper == null)) {
+            LinkedHashMap<String, IndexKeyDefinition> idxDef = idxProc.indexDefinition(idx.id()).indexKeyDefinitions();
+
+            treeFilter = new IndexQueryCriteriaClosure(
+                qry, idxDef, ((SortedIndexDefinition)idxProc.indexDefinition(idx.id())).rowComparator());
+        }
+
+        IndexQueryContext qryCtx = new IndexQueryContext(cacheFilter, treeFilter, null);
+
         if (segmentsCnt == 1)
             return treeIndexRange(idx, 0, qry, qryCtx);
 
@@ -507,76 +528,29 @@
     private GridCursor<IndexRow> treeIndexRange(SortedSegmentedIndex idx, int segment, IndexRangeQuery qry, IndexQueryContext qryCtx)
         throws IgniteCheckedException {
 
-        LinkedHashMap<String, IndexKeyDefinition> idxDef = idxProc.indexDefinition(idx.id()).indexKeyDefinitions();
+        boolean lowIncl = inclBoundary(qry, true);
+        boolean upIncl = inclBoundary(qry, false);
 
-        // Step 1. Traverse index.
-        GridCursor<IndexRow> findRes = idx.find(qry.lower, qry.upper, segment, qryCtx);
+        return idx.find(qry.lower, qry.upper, lowIncl, upIncl, segment, qryCtx);
+    }
 
-        // Step 2. Scan and filter.
-        return new GridCursor<IndexRow>() {
-            /** */
-            private final IndexRowComparator rowCmp = ((SortedIndexDefinition)idxProc.indexDefinition(idx.id())).rowComparator();
+    /**
+     * Checks whether index thraversing should include boundary or not. Includes a boundary for unbounded searches, for
+     * others it checks user criteria.
+     *
+     * @param lower {@code true} for lower bound and {@code false} for upper bound.
+     * @return {@code true} for inclusive boundary, otherwise {@code false}.
+     */
+    private boolean inclBoundary(IndexRangeQuery qry, boolean lower) {
+        for (RangeIndexQueryCriterion c: qry.criteria) {
+            if (c == null || (lower ? c.lower() : c.upper()) == null)
+                break;
 
-            /** {@inheritDoc} */
-            @Override public boolean next() throws IgniteCheckedException {
-                if (!findRes.next())
-                    return false;
-
-                while (rowIsOutOfRange(get(), qry.lower, qry.upper)) {
-                    if (!findRes.next())
-                        return false;
-                }
-
-                return true;
-            }
-
-            /** {@inheritDoc} */
-            @Override public IndexRow get() throws IgniteCheckedException {
-                return findRes.get();
-            }
-
-            /**
-             * Checks that {@code row} belongs to the range specified with {@code lower} and {@code upper}.
-             *
-             * @return {@code true} if the row doesn't belong the range, otherwise {@code false}.
-             */
-            private boolean rowIsOutOfRange(IndexRow row, IndexRow low, IndexRow high) throws IgniteCheckedException {
-                if (low == null && high == null)
-                    return false;  // Unbounded search, include all.
-
-                int criteriaKeysCnt = qry.criteria.length;
-
-                for (int i = 0; i < criteriaKeysCnt; i++) {
-                    RangeIndexQueryCriterion c = qry.criteria[i];
-
-                    boolean descOrder = idxDef.get(c.field()).order().sortOrder() == DESC;
-
-                    if (low != null && low.key(i) != null) {
-                        int cmp = rowCmp.compareRow(row, low, i);
-
-                        if (cmp == 0) {
-                            if (!c.lowerIncl())
-                                return true;  // Exclude if field equals boundary field and criteria is excluding.
-                        }
-                        else if ((cmp < 0) ^ descOrder)
-                            return true;  // Out of bound. Either below 'low' margin or column with desc order.
-                    }
-
-                    if (high != null && high.key(i) != null) {
-                        int cmp = rowCmp.compareRow(row, high, i);
-
-                        if (cmp == 0) {
-                            if (!c.upperIncl())
-                                return true;  // Exclude if field equals boundary field and criteria is excluding.
-                        }
-                        else if ((cmp > 0) ^ descOrder)
-                            return true;  // Out of bound. Either above 'high' margin or column with desc order.
-                    }
-                }
-
+            if (!(lower ? c.lowerIncl() : c.upperIncl()))
                 return false;
-            }
-        };
+        }
+
+        return true;
     }
 
     /**
@@ -624,7 +598,6 @@
                         }
 
                         return 0;
-
                     } catch (IgniteCheckedException e) {
                         throw new IgniteException("Failed to sort remote index rows", e);
                     }
@@ -648,7 +621,7 @@
 
             head = c.get();
 
-            if (c != null && c.next())
+            if (c.next())
                 cursors.add(c);
 
             return true;
@@ -661,6 +634,108 @@
     }
 
     /**
+     * Checks index rows for matching to specified index criteria.
+     */
+    private static class IndexQueryCriteriaClosure implements BPlusTree.TreeRowClosure<IndexRow, IndexRow> {
+        /** */
+        private final IndexRangeQuery qry;
+
+        /** */
+        private final IndexRowComparator rowCmp;
+
+        /** */
+        private final boolean[] descOrderCache;
+
+        /** */
+        IndexQueryCriteriaClosure(
+            IndexRangeQuery qry,
+            LinkedHashMap<String, IndexKeyDefinition> idxDef,
+            IndexRowComparator rowCmp
+        ) {
+            this.qry = qry;
+            this.rowCmp = rowCmp;
+            descOrderCache = new boolean[qry.criteria.length];
+
+            for (int i = 0; i < qry.criteria.length; i++) {
+                RangeIndexQueryCriterion c = qry.criteria[i];
+
+                descOrderCache[i] = idxDef.get(c.field()).order().sortOrder() == DESC;
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean apply(
+            BPlusTree<IndexRow, IndexRow> tree,
+            BPlusIO<IndexRow> io,
+            long pageAddr,
+            int idx
+        ) throws IgniteCheckedException {
+            return !rowIsOutOfRange((InlineIndexTree)tree, io, pageAddr, idx, qry.lower, qry.upper);
+        }
+
+        /**
+         * Checks that {@code row} belongs to the range specified with {@code low} and {@code high}.
+         *
+         * @return {@code true} if the row doesn't belong the range, otherwise {@code false}.
+         */
+        private boolean rowIsOutOfRange(
+            InlineIndexTree tree,
+            BPlusIO<IndexRow> io,
+            long pageAddr,
+            int idx,
+            IndexRow low,
+            IndexRow high
+        ) throws IgniteCheckedException {
+            int criteriaKeysCnt = qry.criteria.length;
+
+            int off = io.offset(idx);
+
+            int fieldOff = 0;
+
+            InlineIndexRow currRow = new InlineIndexRow(tree, io, pageAddr, idx);
+
+            List<InlineIndexKeyType> keyTypes = tree.rowHandler().inlineIndexKeyTypes();
+
+            for (int keyIdx = 0; keyIdx < criteriaKeysCnt; keyIdx++) {
+                RangeIndexQueryCriterion c = qry.criteria[keyIdx];
+
+                InlineIndexKeyType keyType = keyIdx < keyTypes.size() ? keyTypes.get(keyIdx) : null;
+
+                boolean descOrder = descOrderCache[keyIdx];
+
+                int maxSize = tree.inlineSize() - fieldOff;
+
+                if (low != null && low.key(keyIdx) != null) {
+                    int cmp = currRow.compare(rowCmp, low, keyIdx, off + fieldOff, maxSize, keyType);
+
+                    if (cmp == 0) {
+                        if (!c.lowerIncl())
+                            return true;  // Exclude if field equals boundary field and criteria is excluding.
+                    }
+                    else if ((cmp < 0) ^ descOrder)
+                        return true;  // Out of bound. Either below 'low' margin or column with desc order.
+                }
+
+                if (high != null && high.key(keyIdx) != null) {
+                    int cmp = currRow.compare(rowCmp, high, keyIdx, off + fieldOff, maxSize, keyType);
+
+                    if (cmp == 0) {
+                        if (!c.upperIncl())
+                            return true;  // Exclude if field equals boundary field and criteria is excluding.
+                    }
+                    else if ((cmp > 0) ^ descOrder)
+                        return true;  // Out of bound. Either above 'high' margin or column with desc order.
+                }
+
+                if (keyType != null)
+                    fieldOff += keyType.inlineSize(pageAddr, off + fieldOff);
+            }
+
+            return false;
+        }
+    }
+
+    /**
      * @return Modified description for criterion in case of error.
      */
     private static String rangeDesc(RangeIndexQueryCriterion c, String fldName, Object lower, Object upper) {
@@ -693,4 +768,57 @@
         /** */
         private IndexRow upper;
     }
+
+    /**
+     * Wrapper class over index row. It is suitable for comparison. It tries to check inlined keys first, and fetches a
+     * cache entry only if the inlined information is not full enough for comparison.
+     */
+    private static class InlineIndexRow {
+        /** */
+        private final long pageAddr;
+
+        /** */
+        private final int idx;
+
+        /** */
+        private final InlineIndexTree tree;
+
+        /** */
+        private final BPlusIO<IndexRow> io;
+
+        /** Set it for accessing keys from underlying cache entry. */
+        private IndexRow currRow;
+
+        /** */
+        private InlineIndexRow(InlineIndexTree tree, BPlusIO<IndexRow> io, long addr, int idx) {
+            pageAddr = addr;
+            this.idx = idx;
+            this.tree = tree;
+            this.io = io;
+        }
+
+        /** Compare using inline. {@code keyType} is {@code null} for non-inlined keys. */
+        private int compare(
+            IndexRowComparator rowCmp,
+            IndexRow o,
+            int keyIdx,
+            int off,
+            int maxSize,
+            @Nullable InlineIndexKeyType keyType
+        ) throws IgniteCheckedException {
+            if (currRow == null) {
+                int cmp = COMPARE_UNSUPPORTED;
+
+                if (keyType != null)
+                    cmp = rowCmp.compareKey(pageAddr, off, maxSize, o.key(keyIdx), keyType);
+
+                if (cmp == COMPARE_UNSUPPORTED || cmp == CANT_BE_COMPARE)
+                    currRow = tree.getRow(io, pageAddr, idx);
+                else
+                    return cmp;
+            }
+
+            return rowCmp.compareRow(currRow, o, keyIdx);
+        }
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
index e1699de..39a4230 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
@@ -23,8 +23,6 @@
 import java.io.ObjectOutput;
 import org.apache.ignite.internal.cache.query.index.Order;
 import org.apache.ignite.internal.cache.query.index.SortOrder;
-import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
-import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
 import org.apache.ignite.internal.util.typedef.internal.U;
 
 /**
@@ -75,16 +73,6 @@
         return precision;
     }
 
-    /**
-     * @return {@code true} if specified key's type matches to the current type, otherwise {@code false}.
-     */
-    public boolean validate(IndexKey key) {
-        if (key == NullIndexKey.INSTANCE)
-            return true;
-
-        return idxType == key.type();
-    }
-
     /** {@inheritDoc} */
     @Override public void writeExternal(ObjectOutput out) throws IOException {
         // Send only required info for using in MergeSort algorithm.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowComparator.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowComparator.java
index 25258a0..76c7ffa 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowComparator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowComparator.java
@@ -18,6 +18,8 @@
 package org.apache.ignite.internal.cache.query.index.sorted;
 
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.types.NullableInlineIndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 
 /**
@@ -25,15 +27,16 @@
  */
 public interface IndexRowComparator {
     /**
-     * Compare index keys.
+     * Compare inlined index key with specified key. If it is impossible to compare inlined key, it returns special vals:
+     * {@code COMPARE_UNSUPPORTED} and {@code CANT_BE_COMPARE}, see {@link NullableInlineIndexKeyType}.
      *
      * @param pageAddr address of an index row.
      * @param off offset of an index key.
      * @param maxSize max size to read.
      * @param key key to compare with.
-     * @param curType type of index key.
+     * @param type inline type of index key.
      */
-    public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, int curType) throws IgniteCheckedException;
+    public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, InlineIndexKeyType type) throws IgniteCheckedException;
 
     /**
      * Compare index keys.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
index f42a9ce..fa01dd5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
@@ -19,6 +19,7 @@
 package org.apache.ignite.internal.cache.query.index.sorted;
 
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
 
@@ -33,18 +34,39 @@
  * 2. Comparison of different types is not supported.
  */
 public class IndexRowCompartorImpl implements IndexRowComparator {
+    /** Key type settings for this index. */
+    protected final IndexKeyTypeSettings keyTypeSettings;
+
+    /** */
+    public IndexRowCompartorImpl(IndexKeyTypeSettings settings) {
+        keyTypeSettings = settings;
+    }
+
     /** {@inheritDoc} */
-    @Override public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, int curType) {
-        if (curType == IndexKeyTypes.UNKNOWN)
+    @Override public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, InlineIndexKeyType type) {
+        if (type.type() == IndexKeyTypes.UNKNOWN)
             return CANT_BE_COMPARE;
 
-        if (key == NullIndexKey.INSTANCE)
-            return 1;
+        // Value can be set up by user in query with different data type. Don't compare different types in this comparator.
+        if (sameType(key, type.type())) {
+            // If inlining of POJO is not supported then don't compare it here.
+            if (type.type() != IndexKeyTypes.JAVA_OBJECT || keyTypeSettings.inlineObjSupported())
+                return type.compare(pageAddr, off, maxSize, key);
+            else
+                return CANT_BE_COMPARE;
+        }
 
-        // Check that types are different before that.
         return COMPARE_UNSUPPORTED;
     }
 
+    /** */
+    private boolean sameType(IndexKey key, int idxType) {
+        if (key == NullIndexKey.INSTANCE)
+            return true;
+
+        return idxType == key.type();
+    }
+
     /** {@inheritDoc} */
     @Override public int compareRow(IndexRow left, IndexRow right, int idx) throws IgniteCheckedException {
         return compare(left.key(idx), right.key(idx));
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
index 3c6e8c7..e119f0c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
@@ -28,26 +28,24 @@
  */
 public interface SortedSegmentedIndex extends Index {
     /**
-     * Finds index rows by specified range in specified tree segment. Range can be bound or unbound.
-     *
-     * @param lower Nullable lower bound.
-     * @param upper Nullable upper bound.
-     * @param segment Number of tree segment to find.
-     * @return Cursor of found index rows.
-     */
-    public GridCursor<IndexRow> find(@Nullable IndexRow lower, @Nullable IndexRow upper, int segment) throws IgniteCheckedException;
-
-    /**
      * Finds index rows by specified range in specifed tree segment with cache filtering. Range can be bound or unbound.
      *
      * @param lower Nullable lower bound.
      * @param upper Nullable upper bound.
+     * @param lowerIncl {@code true} for inclusive lower bound, otherwise {@code false}.
+     * @param upperIncl {@code true} for inclusive upper bound, otherwise {@code false}.
      * @param segment Number of tree segment to find.
-     * @param qryCtx External index qyery context.
+     * @param qryCtx External index query context.
      * @return Cursor of found index rows.
      */
-    public GridCursor<IndexRow> find(IndexRow lower, IndexRow upper, int segment, IndexQueryContext qryCtx)
-        throws IgniteCheckedException;
+    public GridCursor<IndexRow> find(
+        @Nullable IndexRow lower,
+        @Nullable IndexRow upper,
+        boolean lowerIncl,
+        boolean upperIncl,
+        int segment,
+        IndexQueryContext qryCtx
+    ) throws IgniteCheckedException;
 
     /**
      * Finds first index row for specified tree segment and cache filter.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/defragmentation/DefragIndexFactory.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/defragmentation/DefragIndexFactory.java
index 159d062..214663c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/defragmentation/DefragIndexFactory.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/defragmentation/DefragIndexFactory.java
@@ -242,6 +242,8 @@
 
         /** {@inheritDoc} */
         @Override public void storeByOffset(long pageAddr, int off, IndexRow row) throws IgniteCheckedException {
+            assertPageType(pageAddr);
+
             DefragIndexFactory.storeByOffset(io, pageAddr, off, (DefragIndexRowImpl)row);
         }
 
@@ -306,6 +308,8 @@
 
         /** {@inheritDoc} */
         @Override public void storeByOffset(long pageAddr, int off, IndexRow row) throws IgniteCheckedException {
+            assertPageType(pageAddr);
+
             DefragIndexFactory.storeByOffset(io, pageAddr, off, (DefragIndexRowImpl)row);
         }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
index 99c6b81..2e27c70 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
@@ -17,21 +17,31 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted.inline;
 
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
+import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
 import org.apache.ignite.spi.indexing.IndexingQueryFilter;
 
 /** */
 public class IndexQueryContext {
-    /** Cache entry filter. */
-    private final IndexingQueryFilter filter;
+    /** Cache filter. */
+    private final IndexingQueryFilter cacheFilter;
+
+    /** Index rows filter. */
+    private final BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter;
 
     /** */
     private final MvccSnapshot mvccSnapshot;
 
     /** */
-    public IndexQueryContext(IndexingQueryFilter filter, MvccSnapshot snapshot) {
-        this.filter = filter;
-        this.mvccSnapshot = snapshot;
+    public IndexQueryContext(
+        IndexingQueryFilter cacheFilter,
+        BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter,
+        MvccSnapshot mvccSnapshot
+    ) {
+        this.cacheFilter = cacheFilter;
+        this.rowFilter = rowFilter;
+        this.mvccSnapshot = mvccSnapshot;
     }
 
     /**
@@ -42,9 +52,16 @@
     }
 
     /**
-     * @return Filter.
+     * @return Cache filter.
      */
-    public IndexingQueryFilter filter() {
-        return filter;
+    public IndexingQueryFilter cacheFilter() {
+        return cacheFilter;
+    }
+
+    /**
+     * @return Index row filter.
+     */
+    public BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter() {
+        return rowFilter;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
index 7bf3fc4..b93b3d0 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
@@ -83,14 +83,11 @@
     }
 
     /** {@inheritDoc} */
-    @Override public GridCursor<IndexRow> find(IndexRow lower, IndexRow upper, int segment) throws IgniteCheckedException {
-        return find(lower, upper, segment, null);
-    }
-
-    /** {@inheritDoc} */
     @Override public GridCursor<IndexRow> find(
         IndexRow lower,
         IndexRow upper,
+        boolean lowIncl,
+        boolean upIncl,
         int segment,
         IndexQueryContext qryCtx
     ) throws IgniteCheckedException {
@@ -106,7 +103,7 @@
             return new SingleCursor<>(row);
         }
 
-        return segments[segment].find(lower, upper, closure, null);
+        return segments[segment].find(lower, upper, lowIncl, upIncl, closure, null);
     }
 
     /** {@inheritDoc} */
@@ -367,8 +364,8 @@
         if (qryCtx == null)
             return null;
 
-        IndexingQueryCacheFilter cacheFilter = qryCtx.filter() == null ? null
-            : qryCtx.filter().forCache(cctx.cache().name());
+        IndexingQueryCacheFilter cacheFilter = qryCtx.cacheFilter() == null ? null
+            : qryCtx.cacheFilter().forCache(cctx.cache().name());
 
         MvccSnapshot v = qryCtx.mvccSnapshot();
 
@@ -378,7 +375,7 @@
             return null;
 
         return new InlineTreeFilterClosure(
-            cacheFilter, v, cctx, cctx.kernalContext().config().getGridLogger());
+            cacheFilter, qryCtx.rowFilter(), v, cctx, cctx.kernalContext().config().getGridLogger());
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
index b61b8a6..d6f90d1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
@@ -64,12 +64,17 @@
      * @param off Offset.
      * @param key Index key.
      * @param maxSize Max size.
-     *
      * @return Amount of bytes actually stored.
      */
     public int put(long pageAddr, int off, IndexKey key, int maxSize);
 
     /**
+     * Gets index key from inline index tree.
+     *
+     * @param pageAddr Page address.
+     * @param off Offset.
+     * @param maxSize Max size.
+     * @return Index key extracted from index tree.
      */
     @Nullable public IndexKey get(long pageAddr, int off, int maxSize);
 
@@ -80,7 +85,6 @@
      * @param off Offset.
      * @param maxSize Max size.
      * @param v Value that should be compare.
-     *
      * @return -1, 0 or 1 if inlined value less, equal or greater
      * than given respectively, or -2 if inlined part is not enough to compare.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
index b87332d..fb99fb4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
@@ -27,7 +27,6 @@
 import org.apache.ignite.internal.cache.query.index.SortOrder;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypes;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRowCache;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRowImpl;
@@ -78,9 +77,6 @@
     /** Amount of bytes to store inlined index keys. */
     private final int inlineSize;
 
-    /** Index key type settings for this tree. */
-    private final IndexKeyTypeSettings keyTypeSettings;
-
     /** Recommends change inline size if needed. */
     private final InlineRecommender recommender;
 
@@ -192,8 +188,6 @@
             setIos(inlineSize, mvccEnabled);
         }
 
-        this.keyTypeSettings = keyTypeSettings;
-
         initTree(initNew, inlineSize);
 
         this.recommender = recommender;
@@ -292,38 +286,18 @@
 
                 InlineIndexKeyType keyType = keyTypes.get(keyIdx);
 
-                IndexKeyDefinition keyDef = keyDefs.get(keyIdx);
-
-                int cmp = COMPARE_UNSUPPORTED;
-
-                // Value can be set up by user in query with different data type.
-                // By default do not compare different types.
-                if (keyDef.validate(row.key(keyIdx))) {
-                    if (keyType.type() != IndexKeyTypes.JAVA_OBJECT || keyTypeSettings.inlineObjSupported()) {
-                        cmp = keyType.compare(pageAddr, off + fieldOff, maxSize, row.key(keyIdx));
-
-                        fieldOff += keyType.inlineSize(pageAddr, off + fieldOff);
-                    }
-                    // If inlining of POJO is not supported then fallback to previous logic.
-                    else
-                        break;
-                }
-
-                // Can't compare as inlined bytes are not enough for comparation.
-                if (cmp == CANT_BE_COMPARE)
-                    break;
-
-                // Try compare stored values for inlined keys with different approach?
-                if (cmp == COMPARE_UNSUPPORTED)
-                    cmp = def.rowComparator().compareKey(
-                        pageAddr, off + fieldOff, maxSize, row.key(keyIdx), keyType.type());
+                int cmp = def.rowComparator().compareKey(pageAddr, off + fieldOff, maxSize, row.key(keyIdx), keyType);
 
                 if (cmp == CANT_BE_COMPARE || cmp == COMPARE_UNSUPPORTED)
                     break;
+                else
+                    fieldOff += keyType.inlineSize(pageAddr, off + fieldOff);
 
-                if (cmp != 0)
+                if (cmp != 0) {
+                    IndexKeyDefinition keyDef = keyDefs.get(keyIdx);
+
                     return applySortOrder(cmp, keyDef.order().sortOrder());
-
+                }
             } catch (Exception e) {
                 throw new IgniteException("Failed to store new index row.", e);
             }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
index a57395f..3ff5812 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
@@ -45,20 +45,29 @@
     private final MvccSnapshot mvccSnapshot;
 
     /** */
-    private final IndexingQueryCacheFilter filter;
+    private final IndexingQueryCacheFilter cacheFilter;
 
     /** */
-    private final GridCacheContext cctx;
+    private final BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter;
+
+    /** */
+    private final GridCacheContext<?, ?> cctx;
 
     /** */
     private final IgniteLogger log;
 
     /** Constructor. */
-    public InlineTreeFilterClosure(IndexingQueryCacheFilter filter, MvccSnapshot mvccSnapshot,
-        GridCacheContext<?, ?> cctx, IgniteLogger log) {
-        assert (filter != null || mvccSnapshot != null) && cctx != null;
+    public InlineTreeFilterClosure(
+        IndexingQueryCacheFilter cacheFilter,
+        BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter,
+        MvccSnapshot mvccSnapshot,
+        GridCacheContext<?, ?> cctx,
+        IgniteLogger log
+    ) {
+        assert (cacheFilter != null || mvccSnapshot != null) && cctx != null;
 
-        this.filter = filter;
+        this.cacheFilter = cacheFilter;
+        this.rowFilter = rowFilter;
         this.mvccSnapshot = mvccSnapshot;
         this.cctx = cctx;
         this.log = log;
@@ -68,10 +77,16 @@
     @Override public boolean apply(BPlusTree<IndexRow, IndexRow> tree, BPlusIO<IndexRow> io,
         long pageAddr, int idx) throws IgniteCheckedException {
 
-        boolean val = filter == null || applyFilter((InlineIO)io, pageAddr, idx);
+        boolean val = cacheFilter == null || applyFilter((InlineIO)io, pageAddr, idx);
 
-        if (mvccSnapshot != null)
-            return val && applyMvcc((InlineIO)io, pageAddr, idx);
+        if (!val)
+            return false;
+
+        if (rowFilter != null)
+            val = rowFilter.apply(tree, io, pageAddr, idx);
+
+        if (val && mvccSnapshot != null)
+            return applyMvcc((InlineIO)io, pageAddr, idx);
 
         return val;
     }
@@ -83,9 +98,9 @@
      * @return {@code True} if row passes the filter.
      */
     private boolean applyFilter(InlineIO io, long pageAddr, int idx) {
-        assert filter != null;
+        assert cacheFilter != null;
 
-        return filter.applyPartition(PageIdUtils.partId(pageId(io.link(pageAddr, idx))));
+        return cacheFilter.applyPartition(PageIdUtils.partId(pageId(io.link(pageAddr, idx))));
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineInnerIO.java
index 8d2900b..692b661 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineInnerIO.java
@@ -83,6 +83,7 @@
     @SuppressWarnings("ForLoopReplaceableByForEach")
     @Override public final void storeByOffset(long pageAddr, int off, IndexRow row) {
         assert row.link() != 0 : row;
+        assertPageType(pageAddr);
 
         int fieldOff = 0;
 
@@ -129,6 +130,8 @@
 
     /** {@inheritDoc} */
     @Override public final void store(long dstPageAddr, int dstIdx, BPlusIO<IndexRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         int srcOff = srcIo.offset(srcIdx);
 
         byte[] payload = PageUtils.getBytes(srcPageAddr, srcOff, inlineSize);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineLeafIO.java
index 7754031..55c7411 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInlineLeafIO.java
@@ -83,6 +83,7 @@
     @SuppressWarnings("ForLoopReplaceableByForEach")
     @Override public final void storeByOffset(long pageAddr, int off, IndexRow row) {
         assert row.link() != 0 : row;
+        assertPageType(pageAddr);
 
         int fieldOff = 0;
 
@@ -129,6 +130,8 @@
 
     /** {@inheritDoc} */
     @Override public final void store(long dstPageAddr, int dstIdx, BPlusIO<IndexRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         int srcOff = srcIo.offset(srcIdx);
 
         byte[] payload = PageUtils.getBytes(srcPageAddr, srcOff, inlineSize);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInnerIO.java
index c1129cf..d2bbbf5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractInnerIO.java
@@ -41,6 +41,7 @@
     /** {@inheritDoc} */
     @Override public void storeByOffset(long pageAddr, int off, IndexRow row) {
         assert row.link() != 0;
+        assertPageType(pageAddr);
 
         IORowHandler.store(pageAddr, off, row, storeMvccInfo());
     }
@@ -66,6 +67,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<IndexRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         IORowHandler.store(dstPageAddr, offset(dstIdx), (InlineIO)srcIo, srcPageAddr, srcIdx, storeMvccInfo());
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractLeafIO.java
index da4030d8..811b333 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/io/AbstractLeafIO.java
@@ -41,6 +41,7 @@
     /** {@inheritDoc} */
     @Override public void storeByOffset(long pageAddr, int off, IndexRow row) {
         assert row.link() != 0;
+        assertPageType(pageAddr);
 
         IORowHandler.store(pageAddr, off, row, storeMvccInfo());
     }
@@ -66,6 +67,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<IndexRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         IORowHandler.store(dstPageAddr, offset(dstIdx), (InlineIO)srcIo, srcPageAddr, srcIdx, storeMvccInfo());
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesCompareUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesCompareUtils.java
deleted file mode 100644
index c2a8310..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesCompareUtils.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.cache.query.index.sorted.keys;
-
-/** */
-public final class BytesCompareUtils {
-    /** */
-    public static int compareNotNullSigned(byte[] arr0, byte[] arr1) {
-        if (arr0 == arr1)
-            return 0;
-        else {
-            int commonLen = Math.min(arr0.length, arr1.length);
-
-            for (int i = 0; i < commonLen; ++i) {
-                byte b0 = arr0[i];
-                byte b1 = arr1[i];
-
-                if (b0 != b1)
-                    return b0 > b1 ? 1 : -1;
-            }
-
-            return Integer.signum(arr0.length - arr1.length);
-        }
-    }
-
-    /** */
-    public static int compareNotNullUnsigned(byte[] arr0, byte[] arr1) {
-        if (arr0 == arr1)
-            return 0;
-        else {
-            int commonLen = Math.min(arr0.length, arr1.length);
-
-            for (int i = 0; i < commonLen; ++i) {
-                int unSignArr0 = arr0[i] & 255;
-                int unSignArr1 = arr1[i] & 255;
-
-                if (unSignArr0 != unSignArr1)
-                    return unSignArr0 > unSignArr1 ? 1 : -1;
-            }
-
-            return Integer.signum(arr0.length - arr1.length);
-        }
-    }
-}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
index ed9a2db..0eaed2b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
@@ -41,6 +41,24 @@
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        return BytesCompareUtils.compareNotNullUnsigned(key, ((BytesIndexKey)o).key);
+        byte[] arr0 = key;
+        byte[] arr1 = ((BytesIndexKey)o).key;
+
+        if (arr0 == arr1)
+            return 0;
+
+        int commonLen = Math.min(arr0.length, arr1.length);
+        int unSignArr0;
+        int unSignArr1;
+
+        for (int i = 0; i < commonLen; ++i) {
+            unSignArr0 = arr0[i] & 255;
+            unSignArr1 = arr1[i] & 255;
+
+            if (unSignArr0 != unSignArr1)
+                return unSignArr0 > unSignArr1 ? 1 : -1;
+        }
+
+        return Integer.signum(Integer.compare(arr0.length, arr1.length));
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/JavaObjectIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/JavaObjectIndexKey.java
index e4e869a..4e6fd54 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/JavaObjectIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/JavaObjectIndexKey.java
@@ -19,6 +19,7 @@
 
 import java.util.Arrays;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypes;
+import org.apache.ignite.internal.util.typedef.F;
 
 /**
  * Represents an index key that stores as Java Object.
@@ -58,8 +59,11 @@
             int h1 = o1.hashCode();
             int h2 = o2.hashCode();
 
-            if (h1 == h2)
-                return o1.equals(o2) ? 0 : BytesCompareUtils.compareNotNullSigned(bytesNoCopy(), ((JavaObjectIndexKey)o).bytesNoCopy());
+            if (h1 == h2) {
+                return o1.equals(o2)
+                    ? 0
+                    : Integer.signum(F.compareArrays(bytesNoCopy(), ((JavaObjectIndexKey)o).bytesNoCopy()));
+            }
             else
                 return h1 > h2 ? 1 : -1;
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
index 0e52409..63d94aa 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted.keys;
 
+import org.apache.ignite.internal.util.typedef.F;
+
 /** */
 public class SignedBytesIndexKey extends BytesIndexKey {
     /** */
@@ -26,6 +28,6 @@
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        return BytesCompareUtils.compareNotNullSigned(key, ((BytesIndexKey)o).key);
+        return Integer.signum(F.compareArrays(key, ((BytesIndexKey)o).key));
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cdc/CdcMain.java b/modules/core/src/main/java/org/apache/ignite/internal/cdc/CdcMain.java
index 73bd629..1b6addc 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cdc/CdcMain.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cdc/CdcMain.java
@@ -430,6 +430,9 @@
                 .filesOrDirs(segment.toFile())
                 .addFilter((type, ptr) -> type == DATA_RECORD_V2);
 
+        if (igniteCfg.getDataStorageConfiguration().getPageSize() != 0)
+            builder.pageSize(igniteCfg.getDataStorageConfiguration().getPageSize());
+
         long segmentIdx = segmentIndex(segment);
 
         curSegmentIdx.value(segmentIdx);
@@ -474,7 +477,10 @@
                 if (commit) {
                     assert it.lastRead().isPresent();
 
-                    WALPointer ptr = it.lastRead().get();
+                    WALPointer ptr = it.lastRead().get().next();
+
+                    if (log.isDebugEnabled())
+                        log.debug("Saving state [ptr=" + ptr + ']');
 
                     state.save(ptr);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/router/impl/GridRouterCommandLineStartup.java b/modules/core/src/main/java/org/apache/ignite/internal/client/router/impl/GridRouterCommandLineStartup.java
index fc42b6e..52a5279 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/router/impl/GridRouterCommandLineStartup.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/router/impl/GridRouterCommandLineStartup.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.internal.client.router.GridTcpRouterConfiguration;
 import org.apache.ignite.internal.util.spring.IgniteSpringHelper;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
@@ -49,8 +50,8 @@
      *
      * @param beans Beans loaded from spring configuration file.
      */
-    public void start(Map<Class<?>, Object> beans) {
-        log = (IgniteLogger)beans.get(IgniteLogger.class);
+    public void start(Map<Class<?>, Collection> beans) {
+        log = F.<IgniteLogger>first(beans.get(IgniteLogger.class));
 
         if (log == null) {
             U.error(log, "Failed to find logger definition in application context. Stopping the router.");
@@ -58,7 +59,8 @@
             return;
         }
 
-        GridTcpRouterConfiguration tcpCfg = (GridTcpRouterConfiguration)beans.get(GridTcpRouterConfiguration.class);
+        GridTcpRouterConfiguration tcpCfg =
+            F.<GridTcpRouterConfiguration>first(beans.get(GridTcpRouterConfiguration.class));
 
         if (tcpCfg == null)
             U.warn(log, "TCP router startup skipped (configuration not found).");
@@ -144,10 +146,10 @@
         if (!isLog4jUsed)
             savedHnds = U.addJavaNoOpLogger();
 
-        Map<Class<?>, Object> beans;
+        Map<Class<?>, Collection> beans;
 
         try {
-            beans = spring.loadBeans(cfgUrl, IgniteLogger.class, GridTcpRouterConfiguration.class);
+            beans = spring.loadBeans(cfgUrl, IgniteLogger.class, GridTcpRouterConfiguration.class).get1();
         }
         finally {
             if (isLog4jUsed && t != null)
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 af5eaae..251b319 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
@@ -19,10 +19,11 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.ignite.client.ClientOperationType;
 import org.jetbrains.annotations.Nullable;
 
 /** Operation codes. */
-enum ClientOperation {
+public enum ClientOperation {
     /** Resource close. */
     RESOURCE_CLOSE(0),
 
@@ -180,7 +181,13 @@
     COMPUTE_TASK_FINISHED(6001, ClientNotificationType.COMPUTE_TASK_FINISHED),
 
     /** Invoke service. */
-    SERVICE_INVOKE(7000);
+    SERVICE_INVOKE(7000),
+
+    /** Get service descriptors. */
+    SERVICE_GET_DESCRIPTORS(7001),
+
+    /** Get service descriptors. */
+    SERVICE_GET_DESCRIPTOR(7002);
 
     /** Code. */
     private final int code;
@@ -213,6 +220,136 @@
         return notificationType;
     }
 
+    /**
+     * Converts this internal operation code to public {@link ClientOperationType}.
+     *
+     * @return Corresponding {@link ClientOperationType}, or {@code null} if there is no match.
+     * Some operations, such as {@link #RESOURCE_CLOSE}, do not have a public counterpart.
+     */
+    @Nullable public ClientOperationType toPublicOperationType() {
+        switch (this) {
+            case CACHE_GET_OR_CREATE_WITH_NAME:
+            case CACHE_GET_OR_CREATE_WITH_CONFIGURATION:
+                return ClientOperationType.CACHE_GET_OR_CREATE;
+
+            case CACHE_CREATE_WITH_CONFIGURATION:
+            case CACHE_CREATE_WITH_NAME:
+                return ClientOperationType.CACHE_CREATE;
+
+            case CACHE_PUT:
+                return ClientOperationType.CACHE_PUT;
+
+            case CACHE_GET:
+                return ClientOperationType.CACHE_GET;
+
+            case CACHE_GET_NAMES:
+                return ClientOperationType.CACHE_GET_NAMES;
+
+            case CACHE_DESTROY:
+                return ClientOperationType.CACHE_DESTROY;
+
+            case CACHE_CONTAINS_KEY:
+                return ClientOperationType.CACHE_CONTAINS_KEY;
+
+            case CACHE_CONTAINS_KEYS:
+                return ClientOperationType.CACHE_CONTAINS_KEYS;
+
+            case CACHE_GET_CONFIGURATION:
+                return ClientOperationType.CACHE_GET_CONFIGURATION;
+
+            case CACHE_GET_SIZE:
+                return ClientOperationType.CACHE_GET_SIZE;
+
+            case CACHE_PUT_ALL:
+                return ClientOperationType.CACHE_PUT_ALL;
+
+            case CACHE_GET_ALL:
+                return ClientOperationType.CACHE_GET_ALL;
+
+            case CACHE_REPLACE_IF_EQUALS:
+            case CACHE_REPLACE:
+                return ClientOperationType.CACHE_REPLACE;
+
+            case CACHE_REMOVE_KEY:
+            case CACHE_REMOVE_IF_EQUALS:
+                return ClientOperationType.CACHE_REMOVE_ONE;
+
+            case CACHE_REMOVE_KEYS:
+                return ClientOperationType.CACHE_REMOVE_MULTIPLE;
+
+            case CACHE_REMOVE_ALL:
+                return ClientOperationType.CACHE_REMOVE_EVERYTHING;
+
+            case CACHE_GET_AND_PUT:
+                return ClientOperationType.CACHE_GET_AND_PUT;
+
+            case CACHE_GET_AND_REMOVE:
+                return ClientOperationType.CACHE_GET_AND_REMOVE;
+
+            case CACHE_GET_AND_REPLACE:
+                return ClientOperationType.CACHE_GET_AND_REPLACE;
+
+            case CACHE_PUT_IF_ABSENT:
+                return ClientOperationType.CACHE_PUT_IF_ABSENT;
+
+            case CACHE_GET_AND_PUT_IF_ABSENT:
+                return ClientOperationType.CACHE_GET_AND_PUT_IF_ABSENT;
+
+            case CACHE_CLEAR:
+                return ClientOperationType.CACHE_CLEAR_EVERYTHING;
+
+            case CACHE_CLEAR_KEY:
+                return ClientOperationType.CACHE_CLEAR_ONE;
+
+            case CACHE_CLEAR_KEYS:
+                return ClientOperationType.CACHE_CLEAR_MULTIPLE;
+
+            case QUERY_SCAN:
+                return ClientOperationType.QUERY_SCAN;
+
+            case QUERY_SQL:
+            case QUERY_SQL_FIELDS:
+                return ClientOperationType.QUERY_SQL;
+
+            case QUERY_CONTINUOUS:
+                return ClientOperationType.QUERY_CONTINUOUS;
+
+            case TX_START:
+                return ClientOperationType.TRANSACTION_START;
+
+            case CLUSTER_GET_STATE:
+                return ClientOperationType.CLUSTER_GET_STATE;
+
+            case CLUSTER_CHANGE_STATE:
+                return ClientOperationType.CLUSTER_CHANGE_STATE;
+
+            case CLUSTER_GET_WAL_STATE:
+                return ClientOperationType.CLUSTER_GET_WAL_STATE;
+
+            case CLUSTER_CHANGE_WAL_STATE:
+                return ClientOperationType.CLUSTER_CHANGE_WAL_STATE;
+
+            case CLUSTER_GROUP_GET_NODE_IDS:
+            case CLUSTER_GROUP_GET_NODE_INFO:
+                return ClientOperationType.CLUSTER_GROUP_GET_NODES;
+
+            case COMPUTE_TASK_EXECUTE:
+                return ClientOperationType.COMPUTE_TASK_EXECUTE;
+
+            case SERVICE_INVOKE:
+                return ClientOperationType.SERVICE_INVOKE;
+
+            case SERVICE_GET_DESCRIPTORS:
+                return ClientOperationType.SERVICE_GET_DESCRIPTORS;
+
+            case SERVICE_GET_DESCRIPTOR:
+                return ClientOperationType.SERVICE_GET_DESCRIPTOR;
+
+            default:
+                return null;
+        }
+    }
+
     /** Enum mapping from code to values. */
     private static final Map<Short, ClientOperation> map;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientRetryPolicyContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientRetryPolicyContextImpl.java
new file mode 100644
index 0000000..cc81674
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientRetryPolicyContextImpl.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.internal.client.thin;
+
+import org.apache.ignite.client.ClientConnectionException;
+import org.apache.ignite.client.ClientOperationType;
+import org.apache.ignite.client.ClientRetryPolicyContext;
+import org.apache.ignite.configuration.ClientConfiguration;
+
+/**
+ * Retry policy context.
+ */
+class ClientRetryPolicyContextImpl implements ClientRetryPolicyContext {
+    /** */
+    private final ClientConfiguration configuration;
+
+    /** */
+    private final ClientOperationType operation;
+
+    /** */
+    private final int iteration;
+
+    /** */
+    private final ClientConnectionException exception;
+
+    /**
+     * Constructor.
+     *
+     * @param configuration Configuration.
+     * @param operation Operation.
+     * @param iteration Iteration.
+     * @param exception Exception.
+     */
+    public ClientRetryPolicyContextImpl(ClientConfiguration configuration, ClientOperationType operation, int iteration,
+            ClientConnectionException exception) {
+        this.configuration = configuration;
+        this.operation = operation;
+        this.iteration = iteration;
+        this.exception = exception;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientConfiguration configuration() {
+        return configuration;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientOperationType operation() {
+        return operation;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int iteration() {
+        return iteration;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientConnectionException exception() {
+        return exception;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServiceDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServiceDescriptorImpl.java
new file mode 100644
index 0000000..51a1eea
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServiceDescriptorImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.client.thin;
+
+import java.util.UUID;
+import org.apache.ignite.client.ClientServiceDescriptor;
+import org.apache.ignite.platform.PlatformType;
+import org.apache.ignite.services.Service;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Descriptor of {@link Service}.
+ */
+class ClientServiceDescriptorImpl implements ClientServiceDescriptor {
+    /** */
+    private final String name;
+
+    /** */
+    private final String svcCls;
+
+    /** */
+    private final int totalCnt;
+
+    /** */
+    private final int maxPerNodeCnt;
+
+    /** */
+    private final String cacheName;
+
+    /** */
+    private final UUID originNodeId;
+
+    /** */
+    private final PlatformType platformType;
+
+    /** */
+    ClientServiceDescriptorImpl(
+        String name,
+        String svcCls,
+        int totalCnt,
+        int maxPerNodeCnt,
+        String cacheName,
+        UUID originNodeId,
+        PlatformType platformType
+    ) {
+        this.name = name;
+        this.svcCls = svcCls;
+        this.totalCnt = totalCnt;
+        this.maxPerNodeCnt = maxPerNodeCnt;
+        this.cacheName = cacheName;
+        this.originNodeId = originNodeId;
+        this.platformType = platformType;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return name;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String serviceClass() {
+        return svcCls;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int totalCount() {
+        return totalCnt;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int maxPerNodeCount() {
+        return maxPerNodeCnt;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public String cacheName() {
+        return cacheName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public UUID originNodeId() {
+        return originNodeId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public PlatformType platformType() {
+        return platformType;
+    }
+}
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
index 7f83606..f9fcb37 100644
--- 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
@@ -17,18 +17,29 @@
 
 package org.apache.ignite.internal.client.thin;
 
+import java.io.IOException;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Map;
 import java.util.UUID;
 import org.apache.ignite.client.ClientClusterGroup;
 import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.ClientFeatureNotSupportedByServerException;
+import org.apache.ignite.client.ClientServiceDescriptor;
 import org.apache.ignite.client.ClientServices;
 import org.apache.ignite.internal.binary.BinaryRawWriterEx;
+import org.apache.ignite.internal.binary.BinaryReaderExImpl;
+import org.apache.ignite.internal.processors.service.ServiceCallContextImpl;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.platform.PlatformServiceMethod;
+import org.apache.ignite.platform.PlatformType;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallContextBuilder;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link ClientServices}.
@@ -67,11 +78,83 @@
 
     /** {@inheritDoc} */
     @Override public <T> T serviceProxy(String name, Class<? super T> svcItf, long timeout) {
+        return serviceProxy(name, svcItf, null, timeout);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T serviceProxy(String name, Class<? super T> svcItf, ServiceCallContext callCtx) {
+        return serviceProxy(name, svcItf, callCtx, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T serviceProxy(String name, Class<? super T> svcItf, ServiceCallContext callCtx, long timeout) {
         A.notNullOrEmpty(name, "name");
         A.notNull(svcItf, "svcItf");
+        A.ensure(callCtx == null || callCtx instanceof ServiceCallContextImpl,
+            "\"callCtx\" has an invalid type. Custom implementation of " + ServiceCallContext.class.getSimpleName() +
+                " is not supported. Please use " + ServiceCallContextBuilder.class.getSimpleName() + " to create it.");
 
         return (T)Proxy.newProxyInstance(svcItf.getClassLoader(), new Class[] {svcItf},
-            new ServiceInvocationHandler<>(name, timeout, grp));
+            new ServiceInvocationHandler<>(name, timeout, grp,
+                callCtx == null ? null : ((ServiceCallContextImpl)callCtx).values()));
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<ClientServiceDescriptor> serviceDescriptors() {
+        return ch.service(ClientOperation.SERVICE_GET_DESCRIPTORS,
+            req -> checkGetServiceDescriptorsSupported(req.clientChannel().protocolCtx()),
+            res -> {
+                try (BinaryReaderExImpl reader = utils.createBinaryReader(res.in())) {
+                    int sz = res.in().readInt();
+
+                    Collection<ClientServiceDescriptor> svcs = new ArrayList<>(sz);
+
+                    for (int i = 0; i < sz; i++)
+                        svcs.add(readServiceDescriptor(reader));
+
+                    return svcs;
+                }
+                catch (IOException e) {
+                    throw new ClientException(e);
+                }
+            }
+        );
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientServiceDescriptor serviceDescriptor(String name) {
+        A.notNullOrEmpty(name, "name");
+
+        return ch.service(ClientOperation.SERVICE_GET_DESCRIPTOR,
+            req -> {
+                checkGetServiceDescriptorsSupported(req.clientChannel().protocolCtx());
+
+                try (BinaryRawWriterEx writer = utils.createBinaryWriter(req.out())) {
+                    writer.writeString(name);
+                }
+            },
+            res -> {
+                try (BinaryReaderExImpl reader = utils.createBinaryReader(res.in())) {
+                    return readServiceDescriptor(reader);
+                }
+                catch (IOException e) {
+                    throw new ClientException(e);
+                }
+            }
+        );
+    }
+
+    /** */
+    private ClientServiceDescriptorImpl readServiceDescriptor(BinaryReaderExImpl reader) {
+        return new ClientServiceDescriptorImpl(
+            reader.readString(),
+            reader.readString(),
+            reader.readInt(),
+            reader.readInt(),
+            reader.readString(),
+            reader.readUuid(),
+            reader.readByte() == 0 ? PlatformType.JAVA : PlatformType.DOTNET
+        );
     }
 
     /**
@@ -101,14 +184,25 @@
         /** Cluster group. */
         private final ClientClusterGroupImpl grp;
 
+        /** Service call context attributes. */
+        private final Map<String, Object> callAttrs;
+
         /**
          * @param name Service name.
          * @param timeout Timeout.
+         * @param grp Cluster group.
+         * @param callAttrs Service call context attributes.
          */
-        private ServiceInvocationHandler(String name, long timeout, ClientClusterGroupImpl grp) {
+        private ServiceInvocationHandler(
+            String name,
+            long timeout,
+            ClientClusterGroupImpl grp,
+            @Nullable Map<String, Object> callAttrs
+        ) {
             this.name = name;
             this.timeout = timeout;
             this.grp = grp;
+            this.callAttrs = callAttrs;
         }
 
         /** {@inheritDoc} */
@@ -131,6 +225,9 @@
 
         /**
          * @param ch Payload output channel.
+         * @param nodeIds Node IDs.
+         * @param method Method to call.
+         * @param args Method args.
          */
         private void writeServiceInvokeRequest(
             PayloadOutputChannel ch,
@@ -138,7 +235,8 @@
             Method method,
             Object[] args
         ) {
-            ch.clientChannel().protocolCtx().checkFeatureSupported(ProtocolBitmaskFeature.SERVICE_INVOKE);
+            ch.clientChannel().protocolCtx().checkFeatureSupported(callAttrs != null ?
+                ProtocolBitmaskFeature.SERVICE_INVOKE_CALLCTX : ProtocolBitmaskFeature.SERVICE_INVOKE);
 
             try (BinaryRawWriterEx writer = utils.createBinaryWriter(ch.out())) {
                 writer.writeString(name);
@@ -174,7 +272,23 @@
                         writer.writeObject(args[i]);
                     }
                 }
+
+                if (callAttrs != null)
+                    writer.writeMap(callAttrs);
+                else if (ch.clientChannel().protocolCtx().isFeatureSupported(ProtocolBitmaskFeature.SERVICE_INVOKE_CALLCTX))
+                    writer.writeMap(null);
             }
         }
     }
+
+    /**
+     * Check that Get Service Descriptors API is supported by server.
+     *
+     * @param protocolCtx Protocol context.
+     */
+    private void checkGetServiceDescriptorsSupported(ProtocolContext protocolCtx)
+        throws ClientFeatureNotSupportedByServerException {
+        if (!protocolCtx.isFeatureSupported(ProtocolBitmaskFeature.GET_SERVICE_DESCRIPTORS))
+            throw new ClientFeatureNotSupportedByServerException(ProtocolBitmaskFeature.GET_SERVICE_DESCRIPTORS);
+    }
 }
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 c59374e..90c4633 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
@@ -75,7 +75,7 @@
 /**
  * Shared serialization/deserialization utils.
  */
-final class ClientUtils {
+public final class ClientUtils {
     /** Marshaller. */
     private final ClientBinaryMarshaller marsh;
 
@@ -100,7 +100,7 @@
      * @param out Output stream.
      * @param elemWriter Collection element serializer
      */
-    static <E> void collection(
+    public static <E> void collection(
         Collection<E> col, BinaryOutputStream out,
         BiConsumer<BinaryOutputStream, E> elemWriter
     ) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadOutputChannel.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadOutputChannel.java
index cd2b29c..03247e8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadOutputChannel.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadOutputChannel.java
@@ -17,7 +17,9 @@
 
 package org.apache.ignite.internal.client.thin;
 
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.ignite.internal.binary.streams.BinaryHeapOutputStream;
+import org.apache.ignite.internal.binary.streams.BinaryMemoryAllocator;
 import org.apache.ignite.internal.binary.streams.BinaryOutputStream;
 
 /**
@@ -31,13 +33,17 @@
     private final ClientChannel ch;
 
     /** Output stream. */
-    private final BinaryOutputStream out;
+    private final BinaryHeapOutputStream out;
+
+    /** Close guard. */
+    private final AtomicBoolean closed = new AtomicBoolean();
 
     /**
      * Constructor.
      */
     PayloadOutputChannel(ClientChannel ch) {
-        out = new BinaryHeapOutputStream(INITIAL_BUFFER_CAPACITY);
+        // Disable AutoCloseable on the stream so that out callers don't release the pooled buffer before it is written to the socket.
+        out = new BinaryHeapOutputStream(INITIAL_BUFFER_CAPACITY, BinaryMemoryAllocator.POOLED.chunk(), true);
         this.ch = ch;
     }
 
@@ -57,6 +63,9 @@
 
     /** {@inheritDoc} */
     @Override public void close() {
-        out.close();
+        // Pooled buffer is reusable and should be released once and only once.
+        // Releasing more than once potentially "steals" it from another request.
+        if (closed.compareAndSet(false, true))
+            out.release();
     }
 }
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 0e90c1b..568e68a 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
@@ -20,6 +20,7 @@
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.EnumSet;
+import org.apache.ignite.client.ClientServices;
 
 /**
  * Defines supported bitmask features for thin client.
@@ -46,7 +47,13 @@
     DEFAULT_QRY_TIMEOUT(6),
 
     /** Additional SqlFieldsQuery properties: partitions, updateBatchSize */
-    QRY_PARTITIONS_BATCH_SIZE(7);
+    QRY_PARTITIONS_BATCH_SIZE(7),
+
+    /** Handle of {@link ClientServices#serviceDescriptors()}. */
+    GET_SERVICE_DESCRIPTORS(9),
+
+    /** Invoke service methods with caller context. */
+    SERVICE_INVOKE_CALLCTX(10);
 
     /** */
     private static final EnumSet<ProtocolBitmaskFeature> ALL_FEATURES_AS_ENUM_SET =
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannel.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannel.java
index 1c036b0..e842a5b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannel.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannel.java
@@ -44,6 +44,9 @@
 import org.apache.ignite.client.ClientAuthorizationException;
 import org.apache.ignite.client.ClientConnectionException;
 import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.ClientOperationType;
+import org.apache.ignite.client.ClientRetryPolicy;
+import org.apache.ignite.client.ClientRetryPolicyContext;
 import org.apache.ignite.client.IgniteClientFuture;
 import org.apache.ignite.configuration.ClientConfiguration;
 import org.apache.ignite.configuration.ClientConnectorConfiguration;
@@ -161,9 +164,7 @@
         Consumer<PayloadOutputChannel> payloadWriter,
         Function<PayloadInputChannel, T> payloadReader
     ) throws ClientException, ClientError {
-        return applyOnDefaultChannel(channel ->
-            channel.service(op, payloadWriter, payloadReader)
-        );
+        return applyOnDefaultChannel(channel -> channel.service(op, payloadWriter, payloadReader), op);
     }
 
     /**
@@ -196,7 +197,7 @@
         int attemptsCnt[] = new int[1];
 
         try {
-            ch = applyOnDefaultChannel(channel -> channel, attemptsLimit, v -> attemptsCnt[0] = v );
+            ch = applyOnDefaultChannel(channel -> channel, null, attemptsLimit, v -> attemptsCnt[0] = v);
         } catch (Throwable ex) {
             if (failure != null) {
                 failure.addSuppressed(ex);
@@ -238,13 +239,14 @@
                     else
                         failure0.addSuppressed(err);
 
-                    int leftAttempts = attemptsLimit - attemptsCnt[0];
+                    int attempt = attemptsCnt[0];
+                    int leftAttempts = attemptsLimit - attempt;
 
                     // If it is a first retry then reset attempts (as for initialization we use only 1 attempt).
                     if (failure == null)
                         leftAttempts = getRetryLimit() - 1;
 
-                    if (leftAttempts > 0) {
+                    if (leftAttempts > 0 && shouldRetry(op, attempt, failure0)) {
                         handleServiceAsync(fut, op, payloadWriter, payloadReader, leftAttempts, failure0);
 
                         return null;
@@ -309,8 +311,7 @@
 
             if (affNodeId != null) {
                 return applyOnNodeChannelWithFallback(affNodeId, channel ->
-                    channel.service(op, payloadWriter, payloadReader)
-                );
+                    channel.service(op, payloadWriter, payloadReader), op);
             }
         }
 
@@ -332,8 +333,7 @@
 
             if (affNodeId != null) {
                 return applyOnNodeChannelWithFallback(affNodeId, channel ->
-                    channel.service(op, payloadWriter, payloadReader)
-                );
+                    channel.service(op, payloadWriter, payloadReader), op);
             }
         }
 
@@ -379,7 +379,7 @@
 
                                 int attemptsLimit = getRetryLimit() - 1;
 
-                                if (attemptsLimit == 0) {
+                                if (attemptsLimit == 0 || !shouldRetry(op, 0, failure)) {
                                     fut.completeExceptionally(err);
                                     return null;
                                 }
@@ -699,7 +699,7 @@
             return;
 
         // Apply no-op function. Establish default channel connection.
-        applyOnDefaultChannel(channel -> null);
+        applyOnDefaultChannel(channel -> null, null);
 
         if (partitionAwarenessEnabled)
             initAllChannelsAsync();
@@ -727,14 +727,15 @@
     }
 
     /** */
-    private <T> T applyOnDefaultChannel(Function<ClientChannel, T> function) {
-        return applyOnDefaultChannel(function, getRetryLimit(), DO_NOTHING);
+    private <T> T applyOnDefaultChannel(Function<ClientChannel, T> function, ClientOperation op) {
+        return applyOnDefaultChannel(function, op, getRetryLimit(), DO_NOTHING);
     }
 
     /**
      * Apply specified {@code function} on any of available channel.
      */
     private <T> T applyOnDefaultChannel(Function<ClientChannel, T> function,
+                                        ClientOperation op,
                                         int attemptsLimit,
                                         Consumer<Integer> attemptsCallback) {
         ClientConnectionException failure = null;
@@ -770,6 +771,9 @@
                     failure.addSuppressed(e);
 
                 onChannelFailure(hld, c);
+
+                if (op != null && !shouldRetry(op, attempt, e))
+                    break;
             }
         }
 
@@ -780,7 +784,7 @@
      * Try apply specified {@code function} on a channel corresponding to {@code tryNodeId}.
      * If failed then apply the function on any available channel.
      */
-    private <T> T applyOnNodeChannelWithFallback(UUID tryNodeId, Function<ClientChannel, T> function) {
+    private <T> T applyOnNodeChannelWithFallback(UUID tryNodeId, Function<ClientChannel, T> function, ClientOperation op) {
         ClientChannelHolder hld = nodeChannels.get(tryNodeId);
 
         int retryLimit = getRetryLimit();
@@ -799,12 +803,12 @@
 
                 retryLimit -= 1;
 
-                if (retryLimit == 0)
+                if (retryLimit == 0 || !shouldRetry(op, 0, e))
                     throw e;
             }
         }
 
-        return applyOnDefaultChannel(function, retryLimit, DO_NOTHING);
+        return applyOnDefaultChannel(function, op, retryLimit, DO_NOTHING);
     }
 
     /** Get retry limit. */
@@ -819,6 +823,23 @@
         return clientCfg.getRetryLimit() > 0 ? Math.min(clientCfg.getRetryLimit(), size) : size;
     }
 
+    /** Determines whether specified operation should be retried. */
+    private boolean shouldRetry(ClientOperation op, int iteration, ClientConnectionException exception) {
+        ClientOperationType opType = op.toPublicOperationType();
+
+        if (opType == null)
+            return true; // System operation.
+
+        ClientRetryPolicy plc = clientCfg.getRetryPolicy();
+
+        if (plc == null)
+            return false;
+
+        ClientRetryPolicyContext ctx = new ClientRetryPolicyContextImpl(clientCfg, opType, iteration, exception);
+
+        return plc.shouldRetry(ctx);
+    }
+
     /**
      * Channels holder.
      */
@@ -958,7 +979,7 @@
     }
 
     /**
-     * Get holders reference. For test purposes.
+     * Get holders reference. For test purposes.ClientOperation
      */
     @SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType") // For tests.
     List<ClientChannelHolder> getChannelHolders() {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
index 62df87f..3c028c9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
@@ -246,7 +246,9 @@
         throws ClientException {
         long id = reqId.getAndIncrement();
 
-        try (PayloadOutputChannel payloadCh = new PayloadOutputChannel(this)) {
+        PayloadOutputChannel payloadCh = new PayloadOutputChannel(this);
+
+        try {
             if (closed())
                 throw new ClientConnectionException("Channel is closed");
 
@@ -265,14 +267,16 @@
 
             req.writeInt(0, req.position() - 4); // Actual size.
 
-            // arrayCopy is required, because buffer is pooled, and write is async.
-            write(req.arrayCopy(), req.position());
+            write(req.array(), req.position(), payloadCh::close);
 
             return fut;
         }
         catch (Throwable t) {
             pendingReqs.remove(id);
 
+            // Potential double-close is handled in PayloadOutputChannel.
+            payloadCh.close();
+
             throw t;
         }
     }
@@ -605,7 +609,7 @@
 
             writer.out().writeInt(0, writer.out().position() - 4); // actual size
 
-            write(writer.out().arrayCopy(), writer.out().position());
+            write(writer.out().arrayCopy(), writer.out().position(), null);
         }
     }
 
@@ -675,11 +679,11 @@
     }
 
     /** Write bytes to the output stream. */
-    private void write(byte[] bytes, int len) throws ClientConnectionException {
+    private void write(byte[] bytes, int len, @Nullable Runnable onDone) throws ClientConnectionException {
         ByteBuffer buf = ByteBuffer.wrap(bytes, 0, len);
 
         try {
-            sock.send(buf);
+            sock.send(buf, onDone);
         }
         catch (IgniteCheckedException e) {
             throw new ClientConnectionException(e.getMessage(), e);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/ClientConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/ClientConnection.java
index eed90b6..a31302b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/ClientConnection.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/ClientConnection.java
@@ -20,6 +20,7 @@
 import java.nio.ByteBuffer;
 
 import org.apache.ignite.IgniteCheckedException;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Client connection: abstracts away sending and receiving messages.
@@ -29,8 +30,9 @@
      * Sends a message.
      *
      * @param msg Message buffer.
+     * @param onDone Callback to be invoked when asynchronous send operation completes.
      */
-    void send(ByteBuffer msg) throws IgniteCheckedException;
+    void send(ByteBuffer msg, @Nullable Runnable onDone) throws IgniteCheckedException;
 
     /**
      * Closes the connection.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnection.java
index e81d6f4..5a87444 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnection.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnection.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.internal.client.thin.io.ClientMessageHandler;
 import org.apache.ignite.internal.util.nio.GridNioSession;
 import org.apache.ignite.internal.util.nio.GridNioSessionMetaKey;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Client connection.
@@ -62,8 +63,11 @@
     }
 
     /** {@inheritDoc} */
-    @Override public void send(ByteBuffer msg) throws IgniteCheckedException {
-        ses.sendNoFuture(msg, null);
+    @Override public void send(ByteBuffer msg, @Nullable Runnable onDone) throws IgniteCheckedException {
+        if (onDone != null)
+            ses.send(msg).listen(f -> onDone.run());
+        else
+            ses.sendNoFuture(msg, null);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cluster/graph/FullyConnectedComponentSearcher.java b/modules/core/src/main/java/org/apache/ignite/internal/cluster/graph/FullyConnectedComponentSearcher.java
index 9a8098e..d7f8d75 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cluster/graph/FullyConnectedComponentSearcher.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cluster/graph/FullyConnectedComponentSearcher.java
@@ -21,6 +21,7 @@
 import java.util.BitSet;
 import java.util.Comparator;
 import java.util.Iterator;
+import org.apache.ignite.internal.util.typedef.F;
 
 /**
  * Class to find (possibly) largest fully-connected component (also can be called as <b>complete subgraph</b>) in graph.
@@ -324,18 +325,7 @@
 
         /** {@inheritDoc} */
         @Override public int compare(Integer node1, Integer node2) {
-            long[] conn1 = connectionRow(node1);
-            long[] conn2 = connectionRow(node2);
-
-            int len = Math.min(conn1.length, conn2.length);
-            for (int i = 0; i < len; i++) {
-                int res = Long.compare(conn1[i], conn2[i]);
-
-                if (res != 0)
-                    return res;
-            }
-
-            return conn1.length - conn2.length;
+            return F.compareArrays(connectionRow(node1), connectionRow(node2));
         }
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/collision/GridCollisionManager.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/collision/GridCollisionManager.java
index 2282e60..0e41221 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/collision/GridCollisionManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/collision/GridCollisionManager.java
@@ -104,27 +104,33 @@
     }
 
     /**
-     * @param waitJobs List of waiting jobs.
-     * @param activeJobs List of active jobs.
-     * @param heldJobs List of held jobs.
+     * Invoke collision SPI.
+     *
+     * @param waitJobs Collection of waiting jobs.
+     * @param activeJobs Collection of active jobs.
+     * @param heldJobs Collection of held jobs.
      */
     public void onCollision(
         final Collection<CollisionJobContext> waitJobs,
         final Collection<CollisionJobContext> activeJobs,
-        final Collection<CollisionJobContext> heldJobs) {
+        final Collection<CollisionJobContext> heldJobs
+    ) {
         if (enabled()) {
             if (log.isDebugEnabled())
                 log.debug("Resolving job collisions [waitJobs=" + waitJobs + ", activeJobs=" + activeJobs + ']');
 
             getSpi().onCollision(new CollisionContext() {
+                /** {@inheritDoc} */
                 @Override public Collection<CollisionJobContext> activeJobs() {
                     return activeJobs;
                 }
 
+                /** {@inheritDoc} */
                 @Override public Collection<CollisionJobContext> waitingJobs() {
                     return waitJobs;
                 }
 
+                /** {@inheritDoc} */
                 @Override public Collection<CollisionJobContext> heldJobs() {
                     return heldJobs;
                 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/GridDiscoveryManager.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/GridDiscoveryManager.java
index 14c830f..81f8947 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/GridDiscoveryManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/GridDiscoveryManager.java
@@ -1585,7 +1585,7 @@
 
         assert memCfg != null;
 
-        long res = memCfg.getSystemRegionMaxSize();
+        long res = memCfg.getSystemDataRegionConfiguration().getMaxSize();
 
         // Add memory policies.
         DataRegionConfiguration[] dataRegions = memCfg.getDataRegionConfigurations();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
index 071de5f..5fee7bb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
@@ -23,18 +23,14 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReentrantLock;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.configuration.DataStorageConfiguration;
-import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.NodeStoppingException;
-import org.apache.ignite.internal.managers.communication.GridIoPolicy;
 import org.apache.ignite.internal.pagemem.PageIdAllocator;
 import org.apache.ignite.internal.pagemem.PageIdUtils;
 import org.apache.ignite.internal.pagemem.store.IgnitePageStoreManager;
@@ -51,8 +47,6 @@
 import org.apache.ignite.internal.util.lang.IgniteInClosureX;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.CU;
-import org.apache.ignite.thread.IgniteThreadPoolExecutor;
-import org.apache.ignite.thread.OomExceptionHandler;
 
 import static org.apache.ignite.internal.util.IgniteUtils.MB;
 
@@ -61,9 +55,6 @@
  * Scans a range of pages and marks them as dirty to re-encrypt them with the last encryption key on disk.
  */
 public class CacheGroupPageScanner implements CheckpointListener {
-    /** Thread prefix for scanning tasks. */
-    private static final String REENCRYPT_THREAD_PREFIX = "reencrypt";
-
     /** Kernal context. */
     private final GridKernalContext ctx;
 
@@ -79,9 +70,6 @@
     /** Collection of groups waiting for a checkpoint. */
     private final Collection<GroupScanTask> cpWaitGrps = new ConcurrentLinkedQueue<>();
 
-    /** Single-threaded executor to run cache group scan task. */
-    private final ThreadPoolExecutor singleExecSvc;
-
     /** Number of pages that is scanned during reencryption under checkpoint lock. */
     private final int batchSize;
 
@@ -104,7 +92,6 @@
         if (ctx.clientNode() || !CU.isPersistenceEnabled(dsCfg)) {
             batchSize = -1;
             limiter = null;
-            singleExecSvc = null;
 
             return;
         }
@@ -114,17 +101,6 @@
         limiter = new BasicRateLimiter(calcPermits(rateLimit, dsCfg));
 
         batchSize = dsCfg.getEncryptionConfiguration().getReencryptionBatchSize();
-
-        singleExecSvc = new IgniteThreadPoolExecutor(REENCRYPT_THREAD_PREFIX,
-            ctx.igniteInstanceName(),
-            1,
-            1,
-            IgniteConfiguration.DFLT_THREAD_KEEP_ALIVE_TIME,
-            new LinkedBlockingQueue<>(),
-            GridIoPolicy.SYSTEM_POOL,
-            new OomExceptionHandler(ctx));
-
-        singleExecSvc.allowCoreThreadTimeOut(true);
     }
 
     /** {@inheritDoc} */
@@ -219,7 +195,7 @@
             lock.unlock();
         }
 
-        singleExecSvc.submit(() -> schedule0(grpScanTask));
+        ctx.pools().getReencryptionExecutorService().submit(() -> schedule0(grpScanTask));
 
         return grpScanTask;
     }
@@ -268,9 +244,6 @@
 
             for (GroupScanTask grpScanTask : grps.values())
                 grpScanTask.cancel();
-
-            if (singleExecSvc != null)
-                singleExecSvc.shutdownNow();
         } finally {
             lock.unlock();
         }
@@ -491,7 +464,7 @@
          * @param partId Partition ID.
          */
         private void schedulePartitionScan(int partId) {
-            singleExecSvc.submit(() -> scanPartition(partId));
+            ctx.pools().getReencryptionExecutorService().submit(() -> scanPartition(partId));
         }
 
         /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageManager.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageManager.java
index 6c9aced..59e1b32 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageManager.java
@@ -271,6 +271,11 @@
     @Override public void stop(boolean cancel) throws IgniteCheckedException {
         stopSpi();
 
+        Map<IgnitePredicate<? extends Event>, int[]> evtLsnrs = ctx.config().getLocalEventListeners();
+
+        if (evtLsnrs != null)
+            U.stopLifecycleAware(log, evtLsnrs.keySet());
+
         if (log.isDebugEnabled())
             log.debug(stopInfo());
     }
@@ -280,7 +285,11 @@
         Map<IgnitePredicate<? extends Event>, int[]> evtLsnrs = ctx.config().getLocalEventListeners();
 
         if (evtLsnrs != null) {
-            for (IgnitePredicate<? extends Event> lsnr : evtLsnrs.keySet())
+            Set<IgnitePredicate<? extends Event>> lsnrs = evtLsnrs.keySet();
+
+            U.startLifecycleAware(lsnrs);
+
+            for (IgnitePredicate<? extends Event> lsnr : lsnrs)
                 addLocalEventListener(lsnr, evtLsnrs.get(lsnr));
         }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewMBean.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewMBean.java
index 42ff7c2..97e3b14 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewMBean.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewMBean.java
@@ -54,6 +54,7 @@
 import org.apache.ignite.spi.systemview.view.SystemView;
 import org.apache.ignite.spi.systemview.view.SystemViewRowAttributeWalker.AttributeVisitor;
 import org.apache.ignite.spi.systemview.view.SystemViewRowAttributeWalker.AttributeWithValueVisitor;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * JMX bean to expose specific {@link SystemView} data.
@@ -280,8 +281,10 @@
         }
 
         /** {@inheritDoc} */
-        @Override public <T> void accept(int idx, String name, Class<T> clazz, T val) {
-            if (clazz.isEnum())
+        @Override public <T> void accept(int idx, String name, Class<T> clazz, @Nullable T val) {
+            if (val == null)
+                data.put(name, val);
+            else if (clazz.isEnum())
                 data.put(name, ((Enum<?>)val).name());
             else if (clazz.isAssignableFrom(Class.class))
                 data.put(name, ((Class<?>)val).getName());
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/CacheViewWalker.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/CacheViewWalker.java
index 0b5f1c9..cc98a09 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/CacheViewWalker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/CacheViewWalker.java
@@ -48,54 +48,55 @@
         v.accept(10, "cacheLoaderFactory", String.class);
         v.accept(11, "cacheStoreFactory", String.class);
         v.accept(12, "cacheWriterFactory", String.class);
-        v.accept(13, "dataRegionName", String.class);
-        v.accept(14, "defaultLockTimeout", long.class);
-        v.accept(15, "evictionFilter", String.class);
-        v.accept(16, "evictionPolicyFactory", String.class);
-        v.accept(17, "expiryPolicyFactory", String.class);
-        v.accept(18, "interceptor", String.class);
-        v.accept(19, "isCopyOnRead", boolean.class);
-        v.accept(20, "isEagerTtl", boolean.class);
-        v.accept(21, "isEncryptionEnabled", boolean.class);
-        v.accept(22, "isEventsDisabled", boolean.class);
-        v.accept(23, "isInvalidate", boolean.class);
-        v.accept(24, "isLoadPreviousValue", boolean.class);
-        v.accept(25, "isManagementEnabled", boolean.class);
-        v.accept(26, "isNearCacheEnabled", boolean.class);
-        v.accept(27, "isOnheapCacheEnabled", boolean.class);
-        v.accept(28, "isReadFromBackup", boolean.class);
-        v.accept(29, "isReadThrough", boolean.class);
-        v.accept(30, "isSqlEscapeAll", boolean.class);
-        v.accept(31, "isSqlOnheapCacheEnabled", boolean.class);
-        v.accept(32, "isStatisticsEnabled", boolean.class);
-        v.accept(33, "isStoreKeepBinary", boolean.class);
-        v.accept(34, "isWriteBehindEnabled", boolean.class);
-        v.accept(35, "isWriteThrough", boolean.class);
-        v.accept(36, "maxConcurrentAsyncOperations", int.class);
-        v.accept(37, "maxQueryIteratorsCount", int.class);
-        v.accept(38, "nearCacheEvictionPolicyFactory", String.class);
-        v.accept(39, "nearCacheStartSize", int.class);
-        v.accept(40, "nodeFilter", String.class);
-        v.accept(41, "partitionLossPolicy", PartitionLossPolicy.class);
-        v.accept(42, "queryDetailMetricsSize", int.class);
-        v.accept(43, "queryParallelism", int.class);
-        v.accept(44, "rebalanceBatchSize", int.class);
-        v.accept(45, "rebalanceBatchesPrefetchCount", long.class);
-        v.accept(46, "rebalanceDelay", long.class);
-        v.accept(47, "rebalanceMode", CacheRebalanceMode.class);
-        v.accept(48, "rebalanceOrder", int.class);
-        v.accept(49, "rebalanceThrottle", long.class);
-        v.accept(50, "rebalanceTimeout", long.class);
-        v.accept(51, "sqlIndexMaxInlineSize", int.class);
-        v.accept(52, "sqlOnheapCacheMaxSize", int.class);
-        v.accept(53, "sqlSchema", String.class);
-        v.accept(54, "topologyValidator", String.class);
-        v.accept(55, "writeBehindBatchSize", int.class);
-        v.accept(56, "writeBehindCoalescing", boolean.class);
-        v.accept(57, "writeBehindFlushFrequency", long.class);
-        v.accept(58, "writeBehindFlushSize", int.class);
-        v.accept(59, "writeBehindFlushThreadCount", int.class);
-        v.accept(60, "writeSynchronizationMode", CacheWriteSynchronizationMode.class);
+        v.accept(13, "conflictResolver", String.class);
+        v.accept(14, "dataRegionName", String.class);
+        v.accept(15, "defaultLockTimeout", long.class);
+        v.accept(16, "evictionFilter", String.class);
+        v.accept(17, "evictionPolicyFactory", String.class);
+        v.accept(18, "expiryPolicyFactory", String.class);
+        v.accept(19, "interceptor", String.class);
+        v.accept(20, "isCopyOnRead", boolean.class);
+        v.accept(21, "isEagerTtl", boolean.class);
+        v.accept(22, "isEncryptionEnabled", boolean.class);
+        v.accept(23, "isEventsDisabled", boolean.class);
+        v.accept(24, "isInvalidate", boolean.class);
+        v.accept(25, "isLoadPreviousValue", boolean.class);
+        v.accept(26, "isManagementEnabled", boolean.class);
+        v.accept(27, "isNearCacheEnabled", boolean.class);
+        v.accept(28, "isOnheapCacheEnabled", boolean.class);
+        v.accept(29, "isReadFromBackup", boolean.class);
+        v.accept(30, "isReadThrough", boolean.class);
+        v.accept(31, "isSqlEscapeAll", boolean.class);
+        v.accept(32, "isSqlOnheapCacheEnabled", boolean.class);
+        v.accept(33, "isStatisticsEnabled", boolean.class);
+        v.accept(34, "isStoreKeepBinary", boolean.class);
+        v.accept(35, "isWriteBehindEnabled", boolean.class);
+        v.accept(36, "isWriteThrough", boolean.class);
+        v.accept(37, "maxConcurrentAsyncOperations", int.class);
+        v.accept(38, "maxQueryIteratorsCount", int.class);
+        v.accept(39, "nearCacheEvictionPolicyFactory", String.class);
+        v.accept(40, "nearCacheStartSize", int.class);
+        v.accept(41, "nodeFilter", String.class);
+        v.accept(42, "partitionLossPolicy", PartitionLossPolicy.class);
+        v.accept(43, "queryDetailMetricsSize", int.class);
+        v.accept(44, "queryParallelism", int.class);
+        v.accept(45, "rebalanceBatchSize", int.class);
+        v.accept(46, "rebalanceBatchesPrefetchCount", long.class);
+        v.accept(47, "rebalanceDelay", long.class);
+        v.accept(48, "rebalanceMode", CacheRebalanceMode.class);
+        v.accept(49, "rebalanceOrder", int.class);
+        v.accept(50, "rebalanceThrottle", long.class);
+        v.accept(51, "rebalanceTimeout", long.class);
+        v.accept(52, "sqlIndexMaxInlineSize", int.class);
+        v.accept(53, "sqlOnheapCacheMaxSize", int.class);
+        v.accept(54, "sqlSchema", String.class);
+        v.accept(55, "topologyValidator", String.class);
+        v.accept(56, "writeBehindBatchSize", int.class);
+        v.accept(57, "writeBehindCoalescing", boolean.class);
+        v.accept(58, "writeBehindFlushFrequency", long.class);
+        v.accept(59, "writeBehindFlushSize", int.class);
+        v.accept(60, "writeBehindFlushThreadCount", int.class);
+        v.accept(61, "writeSynchronizationMode", CacheWriteSynchronizationMode.class);
     }
 
     /** {@inheritDoc} */
@@ -113,58 +114,59 @@
         v.accept(10, "cacheLoaderFactory", String.class, row.cacheLoaderFactory());
         v.accept(11, "cacheStoreFactory", String.class, row.cacheStoreFactory());
         v.accept(12, "cacheWriterFactory", String.class, row.cacheWriterFactory());
-        v.accept(13, "dataRegionName", String.class, row.dataRegionName());
-        v.acceptLong(14, "defaultLockTimeout", row.defaultLockTimeout());
-        v.accept(15, "evictionFilter", String.class, row.evictionFilter());
-        v.accept(16, "evictionPolicyFactory", String.class, row.evictionPolicyFactory());
-        v.accept(17, "expiryPolicyFactory", String.class, row.expiryPolicyFactory());
-        v.accept(18, "interceptor", String.class, row.interceptor());
-        v.acceptBoolean(19, "isCopyOnRead", row.isCopyOnRead());
-        v.acceptBoolean(20, "isEagerTtl", row.isEagerTtl());
-        v.acceptBoolean(21, "isEncryptionEnabled", row.isEncryptionEnabled());
-        v.acceptBoolean(22, "isEventsDisabled", row.isEventsDisabled());
-        v.acceptBoolean(23, "isInvalidate", row.isInvalidate());
-        v.acceptBoolean(24, "isLoadPreviousValue", row.isLoadPreviousValue());
-        v.acceptBoolean(25, "isManagementEnabled", row.isManagementEnabled());
-        v.acceptBoolean(26, "isNearCacheEnabled", row.isNearCacheEnabled());
-        v.acceptBoolean(27, "isOnheapCacheEnabled", row.isOnheapCacheEnabled());
-        v.acceptBoolean(28, "isReadFromBackup", row.isReadFromBackup());
-        v.acceptBoolean(29, "isReadThrough", row.isReadThrough());
-        v.acceptBoolean(30, "isSqlEscapeAll", row.isSqlEscapeAll());
-        v.acceptBoolean(31, "isSqlOnheapCacheEnabled", row.isSqlOnheapCacheEnabled());
-        v.acceptBoolean(32, "isStatisticsEnabled", row.isStatisticsEnabled());
-        v.acceptBoolean(33, "isStoreKeepBinary", row.isStoreKeepBinary());
-        v.acceptBoolean(34, "isWriteBehindEnabled", row.isWriteBehindEnabled());
-        v.acceptBoolean(35, "isWriteThrough", row.isWriteThrough());
-        v.acceptInt(36, "maxConcurrentAsyncOperations", row.maxConcurrentAsyncOperations());
-        v.acceptInt(37, "maxQueryIteratorsCount", row.maxQueryIteratorsCount());
-        v.accept(38, "nearCacheEvictionPolicyFactory", String.class, row.nearCacheEvictionPolicyFactory());
-        v.acceptInt(39, "nearCacheStartSize", row.nearCacheStartSize());
-        v.accept(40, "nodeFilter", String.class, row.nodeFilter());
-        v.accept(41, "partitionLossPolicy", PartitionLossPolicy.class, row.partitionLossPolicy());
-        v.acceptInt(42, "queryDetailMetricsSize", row.queryDetailMetricsSize());
-        v.acceptInt(43, "queryParallelism", row.queryParallelism());
-        v.acceptInt(44, "rebalanceBatchSize", row.rebalanceBatchSize());
-        v.acceptLong(45, "rebalanceBatchesPrefetchCount", row.rebalanceBatchesPrefetchCount());
-        v.acceptLong(46, "rebalanceDelay", row.rebalanceDelay());
-        v.accept(47, "rebalanceMode", CacheRebalanceMode.class, row.rebalanceMode());
-        v.acceptInt(48, "rebalanceOrder", row.rebalanceOrder());
-        v.acceptLong(49, "rebalanceThrottle", row.rebalanceThrottle());
-        v.acceptLong(50, "rebalanceTimeout", row.rebalanceTimeout());
-        v.acceptInt(51, "sqlIndexMaxInlineSize", row.sqlIndexMaxInlineSize());
-        v.acceptInt(52, "sqlOnheapCacheMaxSize", row.sqlOnheapCacheMaxSize());
-        v.accept(53, "sqlSchema", String.class, row.sqlSchema());
-        v.accept(54, "topologyValidator", String.class, row.topologyValidator());
-        v.acceptInt(55, "writeBehindBatchSize", row.writeBehindBatchSize());
-        v.acceptBoolean(56, "writeBehindCoalescing", row.writeBehindCoalescing());
-        v.acceptLong(57, "writeBehindFlushFrequency", row.writeBehindFlushFrequency());
-        v.acceptInt(58, "writeBehindFlushSize", row.writeBehindFlushSize());
-        v.acceptInt(59, "writeBehindFlushThreadCount", row.writeBehindFlushThreadCount());
-        v.accept(60, "writeSynchronizationMode", CacheWriteSynchronizationMode.class, row.writeSynchronizationMode());
+        v.accept(13, "conflictResolver", String.class, row.conflictResolver());
+        v.accept(14, "dataRegionName", String.class, row.dataRegionName());
+        v.acceptLong(15, "defaultLockTimeout", row.defaultLockTimeout());
+        v.accept(16, "evictionFilter", String.class, row.evictionFilter());
+        v.accept(17, "evictionPolicyFactory", String.class, row.evictionPolicyFactory());
+        v.accept(18, "expiryPolicyFactory", String.class, row.expiryPolicyFactory());
+        v.accept(19, "interceptor", String.class, row.interceptor());
+        v.acceptBoolean(20, "isCopyOnRead", row.isCopyOnRead());
+        v.acceptBoolean(21, "isEagerTtl", row.isEagerTtl());
+        v.acceptBoolean(22, "isEncryptionEnabled", row.isEncryptionEnabled());
+        v.acceptBoolean(23, "isEventsDisabled", row.isEventsDisabled());
+        v.acceptBoolean(24, "isInvalidate", row.isInvalidate());
+        v.acceptBoolean(25, "isLoadPreviousValue", row.isLoadPreviousValue());
+        v.acceptBoolean(26, "isManagementEnabled", row.isManagementEnabled());
+        v.acceptBoolean(27, "isNearCacheEnabled", row.isNearCacheEnabled());
+        v.acceptBoolean(28, "isOnheapCacheEnabled", row.isOnheapCacheEnabled());
+        v.acceptBoolean(29, "isReadFromBackup", row.isReadFromBackup());
+        v.acceptBoolean(30, "isReadThrough", row.isReadThrough());
+        v.acceptBoolean(31, "isSqlEscapeAll", row.isSqlEscapeAll());
+        v.acceptBoolean(32, "isSqlOnheapCacheEnabled", row.isSqlOnheapCacheEnabled());
+        v.acceptBoolean(33, "isStatisticsEnabled", row.isStatisticsEnabled());
+        v.acceptBoolean(34, "isStoreKeepBinary", row.isStoreKeepBinary());
+        v.acceptBoolean(35, "isWriteBehindEnabled", row.isWriteBehindEnabled());
+        v.acceptBoolean(36, "isWriteThrough", row.isWriteThrough());
+        v.acceptInt(37, "maxConcurrentAsyncOperations", row.maxConcurrentAsyncOperations());
+        v.acceptInt(38, "maxQueryIteratorsCount", row.maxQueryIteratorsCount());
+        v.accept(39, "nearCacheEvictionPolicyFactory", String.class, row.nearCacheEvictionPolicyFactory());
+        v.acceptInt(40, "nearCacheStartSize", row.nearCacheStartSize());
+        v.accept(41, "nodeFilter", String.class, row.nodeFilter());
+        v.accept(42, "partitionLossPolicy", PartitionLossPolicy.class, row.partitionLossPolicy());
+        v.acceptInt(43, "queryDetailMetricsSize", row.queryDetailMetricsSize());
+        v.acceptInt(44, "queryParallelism", row.queryParallelism());
+        v.acceptInt(45, "rebalanceBatchSize", row.rebalanceBatchSize());
+        v.acceptLong(46, "rebalanceBatchesPrefetchCount", row.rebalanceBatchesPrefetchCount());
+        v.acceptLong(47, "rebalanceDelay", row.rebalanceDelay());
+        v.accept(48, "rebalanceMode", CacheRebalanceMode.class, row.rebalanceMode());
+        v.acceptInt(49, "rebalanceOrder", row.rebalanceOrder());
+        v.acceptLong(50, "rebalanceThrottle", row.rebalanceThrottle());
+        v.acceptLong(51, "rebalanceTimeout", row.rebalanceTimeout());
+        v.acceptInt(52, "sqlIndexMaxInlineSize", row.sqlIndexMaxInlineSize());
+        v.acceptInt(53, "sqlOnheapCacheMaxSize", row.sqlOnheapCacheMaxSize());
+        v.accept(54, "sqlSchema", String.class, row.sqlSchema());
+        v.accept(55, "topologyValidator", String.class, row.topologyValidator());
+        v.acceptInt(56, "writeBehindBatchSize", row.writeBehindBatchSize());
+        v.acceptBoolean(57, "writeBehindCoalescing", row.writeBehindCoalescing());
+        v.acceptLong(58, "writeBehindFlushFrequency", row.writeBehindFlushFrequency());
+        v.acceptInt(59, "writeBehindFlushSize", row.writeBehindFlushSize());
+        v.acceptInt(60, "writeBehindFlushThreadCount", row.writeBehindFlushThreadCount());
+        v.accept(61, "writeSynchronizationMode", CacheWriteSynchronizationMode.class, row.writeSynchronizationMode());
     }
 
     /** {@inheritDoc} */
     @Override public int count() {
-        return 61;
+        return 62;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/SnapshotViewWalker.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/SnapshotViewWalker.java
new file mode 100644
index 0000000..bee5fd3
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/systemview/walker/SnapshotViewWalker.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.managers.systemview.walker;
+
+import org.apache.ignite.spi.systemview.view.SnapshotView;
+import org.apache.ignite.spi.systemview.view.SystemViewRowAttributeWalker;
+
+/**
+ * Generated by {@code org.apache.ignite.codegen.SystemViewRowAttributeWalkerGenerator}.
+ * {@link SnapshotView} attributes walker.
+ * 
+ * @see SnapshotView
+ */
+public class SnapshotViewWalker implements SystemViewRowAttributeWalker<SnapshotView> {
+    /** {@inheritDoc} */
+    @Override public void visitAll(AttributeVisitor v) {
+        v.accept(0, "name", String.class);
+        v.accept(1, "consistentId", String.class);
+        v.accept(2, "baselineNodes", String.class);
+        v.accept(3, "cacheGroups", String.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void visitAll(SnapshotView row, AttributeWithValueVisitor v) {
+        v.accept(0, "name", String.class, row.name());
+        v.accept(1, "consistentId", String.class, row.consistentId());
+        v.accept(2, "baselineNodes", String.class, row.baselineNodes());
+        v.accept(3, "cacheGroups", String.class, row.cacheGroups());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int count() {
+        return 4;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryAllocator.java
similarity index 65%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryAllocator.java
index a244658..2d80390 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryAllocator.java
@@ -14,17 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.ignite.internal.mem.unsafe;
 
-package org.apache.ignite.internal.processors.cache.consistency;
+import org.apache.ignite.internal.util.GridUnsafe;
+import org.apache.ignite.mem.MemoryAllocator;
 
-import org.apache.ignite.cache.CacheMode;
-
-/**
- *
- */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+/** */
+public class UnsafeMemoryAllocator implements MemoryAllocator {
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override public long allocateMemory(long size) {
+        return GridUnsafe.allocateMemory(size);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void freeMemory(long addr) {
+        GridUnsafe.freeMemory(addr);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryProvider.java b/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryProvider.java
index 8cb8119..d281912 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryProvider.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryProvider.java
@@ -25,8 +25,9 @@
 import org.apache.ignite.internal.mem.DirectMemoryProvider;
 import org.apache.ignite.internal.mem.DirectMemoryRegion;
 import org.apache.ignite.internal.mem.UnsafeChunk;
-import org.apache.ignite.internal.util.GridUnsafe;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.mem.MemoryAllocator;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Memory provider implementation based on unsafe memory access.
@@ -49,10 +50,22 @@
     /** */
     private int used = 0;
 
+    /** */
+    private final MemoryAllocator allocator;
+
     /**
      * @param log Ignite logger to use.
      */
-    public UnsafeMemoryProvider(IgniteLogger log) {
+    public UnsafeMemoryProvider(@Nullable IgniteLogger log) {
+        this(log, null);
+    }
+
+    /**
+     * @param log Ignite logger to use.
+     * @param allocator Memory allocator. If {@code null}, default {@link UnsafeMemoryAllocator} will be used.
+     */
+    public UnsafeMemoryProvider(@Nullable IgniteLogger log, @Nullable MemoryAllocator allocator) {
+        this.allocator = allocator == null ? new UnsafeMemoryAllocator() : allocator;
         this.log = log;
     }
 
@@ -70,20 +83,21 @@
 
     /** {@inheritDoc} */
     @Override public void shutdown(boolean deallocate) {
+        if (!deallocate) {
+            used = 0;
+
+            return;
+        }
+
         if (regions != null) {
             for (Iterator<DirectMemoryRegion> it = regions.iterator(); it.hasNext(); ) {
                 DirectMemoryRegion chunk = it.next();
 
-                if (deallocate) {
-                    GridUnsafe.freeMemory(chunk.address());
+                allocator.freeMemory(chunk.address());
 
-                    // Safety.
-                    it.remove();
-                }
+                // Safety.
+                it.remove();
             }
-
-            if (!deallocate)
-                used = 0;
         }
     }
 
@@ -100,7 +114,7 @@
         long ptr;
 
         try {
-            ptr = GridUnsafe.allocateMemory(chunkSize);
+            ptr = allocator.allocateMemory(chunkSize);
         }
         catch (IllegalArgumentException e) {
             String msg = "Failed to allocate next memory chunk: " + U.readableSize(chunkSize, true) +
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/WALIterator.java b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/WALIterator.java
index 1daf7b1..577512c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/WALIterator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/WALIterator.java
@@ -20,7 +20,9 @@
 import java.util.Optional;
 import org.apache.ignite.internal.pagemem.wal.record.WALRecord;
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
+import org.apache.ignite.internal.processors.cache.persistence.wal.reader.IgniteWalIteratorFactory.IteratorParametersBuilder;
 import org.apache.ignite.internal.util.lang.GridCloseableIterator;
+import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteBiTuple;
 
 /**
@@ -28,7 +30,9 @@
  */
 public interface WALIterator extends GridCloseableIterator<IgniteBiTuple<WALPointer, WALRecord>> {
     /**
-     * @return Pointer of last read valid record. Empty if no records were read.
+     * @return Pointer to the last record returned by the {@link #next()} method.
+     * If records are filtered by the {@link IteratorParametersBuilder#filter(IgniteBiPredicate)} then
+     * pointer to the last valid record returned.
      */
     public Optional<WALPointer> lastRead();
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/record/DataRecord.java b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/record/DataRecord.java
index 2507fd4..3ed325b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/record/DataRecord.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/wal/record/DataRecord.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 import java.util.List;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 
@@ -31,7 +32,7 @@
 public class DataRecord extends TimeStampRecord {
     /** */
     @GridToStringInclude
-    private List<DataEntry> writeEntries;
+    private Object writeEntries;
 
     /** {@inheritDoc} */
     @Override public RecordType type() {
@@ -60,19 +61,14 @@
     }
 
     /**
-     * @param writeEntry Write entry.
-     */
-    public DataRecord(DataEntry writeEntry, long timestamp) {
-        this(Collections.singletonList(writeEntry), timestamp);
-    }
-
-    /**
      * @param writeEntries Write entries.
      * @param timestamp TimeStamp.
      */
-    public DataRecord(List<DataEntry> writeEntries, long timestamp) {
+    public DataRecord(Object writeEntries, long timestamp) {
         super(timestamp);
 
+        A.notNull(writeEntries, "writeEntries");
+
         this.writeEntries = writeEntries;
     }
 
@@ -90,7 +86,31 @@
      * @return Collection of write entries.
      */
     public List<DataEntry> writeEntries() {
-        return writeEntries == null ? Collections.<DataEntry>emptyList() : writeEntries;
+        if (writeEntries instanceof DataEntry)
+            return Collections.singletonList((DataEntry)writeEntries);
+
+        return (List<DataEntry>)writeEntries;
+    }
+
+    /** @return Count of {@link DataEntry} stored inside this record. */
+    public int entryCount() {
+        return (writeEntries instanceof DataEntry)
+            ? 1
+            : ((List<DataEntry>)writeEntries).size();
+    }
+
+    /**
+     * @param idx Index of element.
+     * @return {@link DataEntry} at the specified position.
+     */
+    public DataEntry get(int idx) {
+        if (writeEntries instanceof DataEntry) {
+            assert idx == 0;
+
+            return (DataEntry)writeEntries;
+        }
+
+        return ((List<DataEntry>)writeEntries).get(idx);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityProcessor.java
index 1f57c17..5f2fc27 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityProcessor.java
@@ -413,6 +413,9 @@
         throws IgniteCheckedException {
         assert cacheName != null;
 
+        if (topVer == null)
+            topVer = ctx.cache().context().exchange().readyAffinityVersion();
+
         IgniteInternalFuture<AffinityInfo> locFetchFut = localAffinityInfo(cacheName, topVer);
 
         if (locFetchFut != null)
@@ -432,10 +435,9 @@
      */
     private IgniteInternalFuture<AffinityInfo> localAffinityInfo(
         String cacheName,
-        @Nullable AffinityTopologyVersion topVer
+        AffinityTopologyVersion topVer
     ) throws IgniteCheckedException {
-        if (topVer == null)
-            topVer = ctx.cache().context().exchange().readyAffinityVersion();
+        assert topVer != null;
 
         AffinityAssignmentKey key = new AffinityAssignmentKey(cacheName, topVer);
 
@@ -496,10 +498,9 @@
      */
     private IgniteInternalFuture<AffinityInfo> remoteAffinityInfo(
         String cacheName,
-        @Nullable AffinityTopologyVersion topVer
+        AffinityTopologyVersion topVer
     ) {
-        if (topVer == null)
-            topVer = ctx.discovery().topologyVersionEx();
+        assert topVer != null;
 
         AffinityAssignmentKey key = new AffinityAssignmentKey(cacheName, topVer);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java
index 31c7262..afc60bd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java
@@ -47,7 +47,6 @@
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
-import org.apache.ignite.internal.processors.cache.GridCacheUtils;
 import org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsExchangeFuture;
 import org.apache.ignite.internal.processors.cache.distributed.dht.preloader.PartitionsExchangeAware;
 import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetastorageLifecycleListener;
@@ -61,6 +60,7 @@
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.internal.util.worker.GridWorker;
@@ -162,7 +162,7 @@
 
     /** Starts processor. */
     public void startProcessor() throws IgniteCheckedException {
-        if (!GridCacheUtils.isPersistenceEnabled(ctx.config())) {
+        if (!ctx.clientNode() && !CU.isPersistenceEnabled(ctx.config())) {
             throw new IgniteCheckedException("Authentication can be enabled only for cluster with enabled persistence."
                 + " Check the DataRegionConfiguration");
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/bulkload/BulkLoadProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/bulkload/BulkLoadProcessor.java
index f391772..af863ea 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/bulkload/BulkLoadProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/bulkload/BulkLoadProcessor.java
@@ -57,7 +57,7 @@
     private final RunningQueryManager runningQryMgr;
 
     /** Query id. */
-    private final Long qryId;
+    private final long qryId;
 
     /** Exception, current load process ended with, or {@code null} if in progress or if succeded. */
     private Exception failReason;
@@ -80,7 +80,7 @@
      * @param tracing Tracing processor.
      */
     public BulkLoadProcessor(BulkLoadParser inputParser, IgniteClosureX<List<?>, IgniteBiTuple<?, ?>> dataConverter,
-        BulkLoadCacheWriter outputStreamer, RunningQueryManager runningQryMgr, Long qryId, Tracing tracing) {
+        BulkLoadCacheWriter outputStreamer, RunningQueryManager runningQryMgr, long qryId, Tracing tracing) {
         this.inputParser = inputParser;
         this.dataConverter = dataConverter;
         this.outputStreamer = outputStreamer;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEntryPredicateContainsValue.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEntryPredicateContainsValue.java
index b5fde21..e404698 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEntryPredicateContainsValue.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEntryPredicateContainsValue.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.cache;
 
 import java.nio.ByteBuffer;
+import java.util.Objects;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
@@ -73,6 +74,9 @@
         Object thisVal = CU.value(this.val, cctx, false);
         Object cacheVal = CU.value(val, cctx, false);
 
+        if (thisVal.getClass().isArray())
+            return Objects.deepEquals(thisVal, cacheVal);
+
         return F.eq(thisVal, cacheVal);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupContext.java
index a7b85e3..6cadb65 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupContext.java
@@ -54,6 +54,7 @@
 import org.apache.ignite.internal.processors.cache.persistence.tree.reuse.ReuseList;
 import org.apache.ignite.internal.processors.cache.query.continuous.CounterSkipContext;
 import org.apache.ignite.internal.processors.metric.GridMetricManager;
+import org.apache.ignite.internal.processors.plugin.IgnitePluginProcessor;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.StripedCompositeReadWriteLock;
 import org.apache.ignite.internal.util.lang.GridPlainRunnable;
@@ -65,6 +66,7 @@
 import org.apache.ignite.lang.IgniteBiInClosure;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.lang.IgnitePredicate;
+import org.apache.ignite.plugin.CacheTopologyValidatorProvider;
 import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.cache.CacheAtomicityMode.ATOMIC;
@@ -186,6 +188,9 @@
     /** Cache group metrics. */
     private final CacheGroupMetricsImpl metrics;
 
+    /** Topology validators. */
+    private final Collection<TopologyValidator> topValidators;
+
     /**
      * @param ctx Context.
      * @param grpId Group ID.
@@ -261,6 +266,8 @@
         }
 
         hasAtomicCaches = ccfg.getAtomicityMode() == ATOMIC;
+
+        topValidators = Collections.unmodifiableCollection(topologyValidators(ccfg, ctx.kernalContext().plugins()));
     }
 
     /**
@@ -727,10 +734,10 @@
     }
 
     /**
-     * @return Configured topology validator.
+     * @return Configured topology validators.
      */
-    @Nullable public TopologyValidator topologyValidator() {
-        return ccfg.getTopologyValidator();
+    public Collection<TopologyValidator> topologyValidators() {
+        return topValidators;
     }
 
     /**
@@ -1297,4 +1304,36 @@
         if (statHolderIdx != IoStatisticsHolderNoOp.INSTANCE)
             ctx.kernalContext().metric().remove(statHolderIdx.metricRegistryName(), destroy);
     }
+
+    /**
+     * @param ccfg Cache configuration.
+     * @param plugins Ignite plugin processor.
+     * @return Comprehensive collection of topology validators for the cache based on its configuration
+     * and plugin extensions.
+     */
+    private Collection<TopologyValidator> topologyValidators(
+        CacheConfiguration<?, ?> ccfg,
+        IgnitePluginProcessor plugins
+    ) {
+        List<TopologyValidator> res = new ArrayList<>();
+
+        TopologyValidator ccfgTopValidator = ccfg.getTopologyValidator();
+
+        if (ccfgTopValidator != null)
+            res.add(ccfgTopValidator);
+
+        CacheTopologyValidatorProvider[] topValidatorProviders = plugins.extensions(CacheTopologyValidatorProvider.class);
+
+        if (F.isEmpty(topValidatorProviders))
+            return res;
+
+        for (CacheTopologyValidatorProvider topValidatorProvider : topValidatorProviders) {
+            TopologyValidator validator = topValidatorProvider.topologyValidator(cacheOrGroupName());
+
+            if (validator != null)
+                res.add(validator);
+        }
+
+        return res;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheObjectUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheObjectUtils.java
index cf36ce1..88eaeaf 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheObjectUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheObjectUtils.java
@@ -20,6 +20,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.util.MutableSingletonList;
 import org.apache.ignite.internal.util.typedef.F;
@@ -81,7 +83,7 @@
      * @param keepBinary Keep binary flag.
      * @return Unwrapped collection.
      */
-    public static Collection<Object> unwrapBinariesIfNeeded(CacheObjectValueContext ctx, Collection<Object> col,
+    public static Collection<Object> unwrapBinariesIfNeeded(CacheObjectValueContext ctx, Collection<?> col,
         boolean keepBinary) {
         return unwrapBinariesIfNeeded(ctx, col, keepBinary, true);
     }
@@ -92,7 +94,7 @@
      * @param cpy Copy flag.
      * @return Unwrapped collection.
      */
-    private static Collection<Object> unwrapKnownCollection(CacheObjectValueContext ctx, Collection<Object> col,
+    private static Collection<Object> unwrapKnownCollection(CacheObjectValueContext ctx, Collection<?> col,
         boolean keepBinary, boolean cpy) {
         Collection<Object> col0 = BinaryUtils.newKnownCollection(col);
 
@@ -133,7 +135,7 @@
      * @param cpy Copy value flag.
      * @return Unwrapped collection.
      */
-    private static Collection<Object> unwrapBinariesIfNeeded(CacheObjectValueContext ctx, Collection<Object> col,
+    private static Collection<Object> unwrapBinariesIfNeeded(CacheObjectValueContext ctx, Collection<?> col,
         boolean keepBinary, boolean cpy) {
         Collection<Object> col0 = BinaryUtils.newKnownCollection(col);
 
@@ -201,8 +203,10 @@
             return unwrapKnownCollection(ctx, (Collection<Object>)o, keepBinary, cpy);
         else if (BinaryUtils.knownMap(o))
             return unwrapBinariesIfNeeded(ctx, (Map<Object, Object>)o, keepBinary, cpy);
-        else if (o instanceof Object[])
+        else if (o instanceof Object[] && !BinaryArray.useBinaryArrays())
             return unwrapBinariesInArrayIfNeeded(ctx, (Object[])o, keepBinary, cpy);
+        else if (o instanceof BinaryArray && !keepBinary)
+            return ((BinaryObject)o).deserialize(ldr);
 
         return o;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheOperationContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheOperationContext.java
index f6786a9..7f22f538 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheOperationContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheOperationContext.java
@@ -20,6 +20,7 @@
 import java.io.Serializable;
 import javax.cache.expiry.ExpiryPolicy;
 import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.jetbrains.annotations.Nullable;
@@ -49,8 +50,8 @@
     /** */
     private final boolean recovery;
 
-    /** Read-repair flag. */
-    private final boolean readRepair;
+    /** Read-repair strategy. */
+    private final ReadRepairStrategy readRepairStrategy;
 
     /** Keep binary flag. */
     private final boolean keepBinary;
@@ -73,7 +74,7 @@
         expiryPlc = null;
         noRetries = false;
         recovery = false;
-        readRepair = false;
+        readRepairStrategy = null;
         dataCenterId = null;
         allowAtomicOpsInTx = DFLT_ALLOW_ATOMIC_OPS_IN_TX;
     }
@@ -83,7 +84,7 @@
      * @param keepBinary Keep binary flag.
      * @param expiryPlc Expiry policy.
      * @param dataCenterId Data center id.
-     * @param readRepair Read-repair flag.
+     * @param readRepairStrategy Read-repair strategy.
      */
     public CacheOperationContext(
         boolean skipStore,
@@ -92,7 +93,7 @@
         boolean noRetries,
         @Nullable Byte dataCenterId,
         boolean recovery,
-        boolean readRepair,
+        @Nullable ReadRepairStrategy readRepairStrategy,
         boolean allowAtomicOpsInTx
     ) {
         this.skipStore = skipStore;
@@ -101,7 +102,7 @@
         this.noRetries = noRetries;
         this.dataCenterId = dataCenterId;
         this.recovery = recovery;
-        this.readRepair = readRepair;
+        this.readRepairStrategy = readRepairStrategy;
         this.allowAtomicOpsInTx = allowAtomicOpsInTx;
     }
 
@@ -132,7 +133,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -166,7 +167,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -191,7 +192,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -207,7 +208,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -223,7 +224,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -239,15 +240,15 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
     /**
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @return New instance of CacheOperationContext with Read Repair flag.
      */
-    public CacheOperationContext setReadRepair(boolean readRepair) {
+    public CacheOperationContext setReadRepairStrategy(ReadRepairStrategy readRepairStrategy) {
         return new CacheOperationContext(
             skipStore,
             keepBinary,
@@ -255,7 +256,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             allowAtomicOpsInTx);
     }
 
@@ -267,10 +268,10 @@
     }
 
     /**
-     * @return Read Repair flag.
+     * @return Read Repair strategy.
      */
-    public boolean readRepair() {
-        return readRepair;
+    public ReadRepairStrategy readRepairStrategy() {
+        return readRepairStrategy;
     }
 
     /**
@@ -291,7 +292,7 @@
             noRetries,
             dataCenterId,
             recovery,
-            readRepair,
+            readRepairStrategy,
             true);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
index a72b444..299b53c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
@@ -177,7 +177,7 @@
         ctx.systemView().registerView(CACHES_VIEW, CACHES_VIEW_DESC,
             new CacheViewWalker(),
             registeredCaches.values(),
-            CacheView::new);
+            (desc) -> new CacheView(desc, ctx));
 
         ctx.systemView().registerView(CACHE_GRPS_VIEW, CACHE_GRPS_VIEW_DESC,
             new CacheGroupViewWalker(),
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GatewayProtectedCacheProxy.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GatewayProtectedCacheProxy.java
index 75d47f7..1a4366c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GatewayProtectedCacheProxy.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GatewayProtectedCacheProxy.java
@@ -41,6 +41,7 @@
 import org.apache.ignite.cache.CacheEntryProcessor;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.Query;
 import org.apache.ignite.cache.query.QueryCursor;
@@ -52,6 +53,7 @@
 import org.apache.ignite.internal.GridKernalState;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccUtils;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
+import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteClosure;
 import org.apache.ignite.lang.IgniteFuture;
@@ -232,7 +234,9 @@
     }
 
     /** {@inheritDoc} */
-    @Override public IgniteCache<K, V> withReadRepair() {
+    @Override public IgniteCache<K, V> withReadRepair(ReadRepairStrategy strategy) {
+        A.notNull(strategy, "strategy");
+
         CacheOperationGate opGate = onEnter();
 
         try {
@@ -245,7 +249,7 @@
                 throw new UnsupportedOperationException("Read-repair is incompatible with near caches.");
 
             if (context().readThrough()) {
-                // Read Repair get operation produces different versions for same entries loaded via readThrough feature.
+                // Entries loaded via readThrough feature have inconsistent versions by design.
                 throw new UnsupportedOperationException("Read-repair is incompatible with caches that use readThrough.");
             }
 
@@ -257,12 +261,10 @@
                     "at least 1 backup configured for cache.");
             }
 
-            boolean readRepair = opCtx.readRepair();
-
-            if (readRepair)
+            if (opCtx.readRepairStrategy() == strategy)
                 return this;
 
-            return new GatewayProtectedCacheProxy<>(delegate, opCtx.setReadRepair(true), lock);
+            return new GatewayProtectedCacheProxy<>(delegate, opCtx.setReadRepairStrategy(strategy), lock);
         }
         finally {
             onLeave(opGate);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheAdapter.java
index 4298729..bc233d2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheAdapter.java
@@ -43,6 +43,7 @@
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.cache.Cache;
 import javax.cache.expiry.ExpiryPolicy;
@@ -59,6 +60,7 @@
 import org.apache.ignite.cache.CacheInterceptor;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.affinity.Affinity;
 import org.apache.ignite.cluster.ClusterGroup;
 import org.apache.ignite.cluster.ClusterNode;
@@ -94,6 +96,7 @@
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtLocalPartition;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtPartitionTopology;
 import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxLocal;
+import org.apache.ignite.internal.processors.cache.distributed.near.consistency.GridCompoundReadRepairFuture;
 import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteConsistencyViolationException;
 import org.apache.ignite.internal.processors.cache.dr.GridCacheDrInfo;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
@@ -116,7 +119,6 @@
 import org.apache.ignite.internal.transactions.IgniteTxTimeoutCheckedException;
 import org.apache.ignite.internal.transactions.TransactionCheckedException;
 import org.apache.ignite.internal.util.GridSerializableMap;
-import org.apache.ignite.internal.util.future.GridCompoundFuture;
 import org.apache.ignite.internal.util.future.GridEmbeddedFuture;
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
@@ -494,7 +496,7 @@
             false,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX);
 
         return new GridCacheProxyImpl<>(ctx, this, opCtx);
@@ -509,7 +511,7 @@
             false,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX);
 
         return new GridCacheProxyImpl<>((GridCacheContext<K1, V1>)ctx, (GridCacheAdapter<K1, V1>)this, opCtx);
@@ -531,7 +533,7 @@
             false,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX);
 
         return new GridCacheProxyImpl<>(ctx, this, opCtx);
@@ -546,7 +548,7 @@
             true,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX);
 
         return new GridCacheProxyImpl<>(ctx, this, opCtx);
@@ -561,7 +563,7 @@
             false,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX);
 
         return new GridCacheProxyImpl<>(ctx, this, opCtx);
@@ -699,12 +701,12 @@
             /*skip values*/true,
             false);
 
-        boolean readRepair = opCtx != null && opCtx.readRepair();
+        ReadRepairStrategy readRepairStrategy = opCtx != null ? opCtx.readRepairStrategy() : null;
 
-        if (readRepair)
+        if (readRepairStrategy != null)
             return getWithRepairAsync(
                 fut,
-                () -> repairAsync(key, opCtx, true),
+                (ks) -> repairAsync(ks, opCtx, true),
                 () -> containsKeyAsync(key));
 
         return fut;
@@ -733,7 +735,7 @@
             /*task name*/null,
             /*deserialize binary*/false,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             /*skip values*/true,
             /*need ver*/false).chain(new CX1<IgniteInternalFuture<Map<K, V>>, Boolean>() {
             @Override public Boolean applyx(IgniteInternalFuture<Map<K, V>> fut) throws IgniteCheckedException {
@@ -1384,7 +1386,7 @@
             taskName,
             /*deserialize cache objects*/true,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             /*skip values*/false,
             /*need ver*/false).get().get(key);
     }
@@ -1402,7 +1404,7 @@
             taskName,
             true,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             /*skip vals*/false,
             /*can remap*/false).chain(new CX1<IgniteInternalFuture<Map<K, V>>, V>() {
             @Override public V applyx(IgniteInternalFuture<Map<K, V>> e) throws IgniteCheckedException {
@@ -1433,7 +1435,7 @@
             taskName,
             !(opCtx != null && opCtx.isKeepBinary()),
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             /*skip values*/false,
             /*need ver*/false);
 
@@ -1536,7 +1538,7 @@
             key,
             !keepBinary,
             false,
-            opCtx != null && opCtx.readRepair());
+            opCtx != null ? opCtx.readRepairStrategy() : null);
 
         if (ctx.config().getInterceptor() != null)
             fut = fut.chain(new CX1<IgniteInternalFuture<V>, V>() {
@@ -1576,7 +1578,7 @@
                 key0,
                 !keepBinary,
                 true,
-                opCtx != null && opCtx.readRepair());
+                opCtx != null ? opCtx.readRepairStrategy() : null);
 
         final boolean intercept = ctx.config().getInterceptor() != null;
 
@@ -1629,7 +1631,7 @@
             !ctx.keepBinary(),
             false,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair());
+            opCtx != null ? opCtx.readRepairStrategy() : null);
 
         if (ctx.config().getInterceptor() != null)
             map = interceptGet(keys, map);
@@ -1660,7 +1662,7 @@
             !ctx.keepBinary(),
             true,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair());
+            opCtx != null ? opCtx.readRepairStrategy() : null);
 
         Collection<CacheEntry<K, V>> res = new HashSet<>();
 
@@ -1699,7 +1701,7 @@
             taskName,
             !(opCtx != null && opCtx.isKeepBinary()),
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             /*skip vals*/false,
             /*need ver*/false);
 
@@ -1741,7 +1743,7 @@
                 taskName,
                 !(opCtx != null && opCtx.isKeepBinary()),
                 opCtx != null && opCtx.recovery(),
-                opCtx != null && opCtx.readRepair(),
+                opCtx != null ? opCtx.readRepairStrategy() : null,
                 /*skip vals*/false,
                 /*need ver*/true));
 
@@ -1885,7 +1887,7 @@
             taskName,
             deserializeBinary,
             opCtx != null && opCtx.recovery(),
-            opCtx != null && opCtx.readRepair(),
+            opCtx != null ? opCtx.readRepairStrategy() : null,
             skipVals,
             needVer).chain(
             new CX1<IgniteInternalFuture<Map<K, V>>, V>() {
@@ -1924,7 +1926,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         boolean skipVals,
         final boolean needVer
     ) {
@@ -1937,7 +1939,7 @@
             taskName,
             deserializeBinary,
             opCtx != null && opCtx.recovery(),
-            readRepair,
+            readRepairStrategy,
             forcePrimary,
             skipVals ? null : expiryPolicy(opCtx != null ? opCtx.expiry() : null),
             skipVals,
@@ -1966,7 +1968,7 @@
         final String taskName,
         final boolean deserializeBinary,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean forcePrimary,
         @Nullable IgniteCacheExpiryPolicy expiry,
         final boolean skipVals,
@@ -1986,7 +1988,7 @@
             skipVals,
             /*keep cache objects*/false,
             recovery,
-            readRepair,
+            readRepairStrategy,
             needVer,
             null,
             null); // TODO IGNITE-7371
@@ -2018,7 +2020,7 @@
         final boolean skipVals,
         final boolean keepCacheObjects,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean needVer,
         @Nullable String txLbl,
         MvccSnapshot mvccSnapshot
@@ -2386,7 +2388,7 @@
                         false,
                         !readThrough,
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         needVer);
                 }
             }, ctx.operationContextPerCall(), /*retry*/false);
@@ -4571,14 +4573,20 @@
 
                             if (ctx.kernalContext().isStopping())
                                 fut0 = new GridFinishedFuture<>(
-                                    new IgniteCheckedException("Operation has been cancelled (node is stopping)."));
+                                    new IgniteCheckedException("Operation has been cancelled (node or cache is stopping)."));
+                            else if (ctx.gate().isStopped())
+                                fut0 = new GridFinishedFuture<>(new CacheStoppedException(ctx.name()));
                             else {
+                                ctx.operationContextPerCall(opCtx);
+                                ctx.shared().txContextReset();
+
                                 try {
-                                    fut0 = op.op(tx0, opCtx).chain(clo);
+                                    fut0 = op.op(tx0).chain(clo);
                                 }
                                 finally {
                                     // It is necessary to clear tx context in this thread as well.
                                     ctx.shared().txContextReset();
+                                    ctx.operationContextPerCall(null);
                                 }
                             }
 
@@ -4610,7 +4618,7 @@
             IgniteInternalFuture<T> f;
 
             try {
-                f = op.op(tx, opCtx).chain(clo);
+                f = op.op(tx).chain(clo);
             }
             finally {
                 // It is necessary to clear tx context in this thread as well.
@@ -4834,7 +4842,7 @@
                 needVer);
         }
         catch (IgniteConsistencyViolationException e) {
-            repairAsync(key, ctx.operationContextPerCall(), false).get();
+            repairAsync(e.keys(), ctx.operationContextPerCall(), false).get();
 
             return repairableGet(key, deserializeBinary, needVer);
         }
@@ -4880,7 +4888,7 @@
         final K key,
         boolean deserializeBinary,
         final boolean needVer,
-        boolean readRepair) {
+        ReadRepairStrategy readRepairStrategy) {
         try {
             checkJta();
         }
@@ -4896,13 +4904,13 @@
             /*skip vals*/false,
             needVer);
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             CacheOperationContext opCtx = ctx.operationContextPerCall();
 
             return getWithRepairAsync(
                 fut,
-                () -> repairAsync(key, opCtx, false),
-                () -> repairableGetAsync(key, deserializeBinary, needVer, readRepair));
+                (ks) -> repairAsync(ks, opCtx, false),
+                () -> repairableGetAsync(key, deserializeBinary, needVer, readRepairStrategy));
         }
 
         return fut;
@@ -4920,14 +4928,14 @@
         boolean deserializeBinary,
         boolean needVer,
         boolean recovery,
-        boolean readRepair) throws IgniteCheckedException {
+        ReadRepairStrategy readRepairStrategy) throws IgniteCheckedException {
         try {
-            return getAll(keys, deserializeBinary, needVer, recovery, readRepair);
+            return getAll(keys, deserializeBinary, needVer, recovery, readRepairStrategy);
         }
         catch (IgniteConsistencyViolationException e) {
-            repairAsync(keys, ctx.operationContextPerCall(), false).get();
+            repairAsync(e.keys(), ctx.operationContextPerCall(), false).get();
 
-            return repairableGetAll(keys, deserializeBinary, needVer, recovery, readRepair);
+            return repairableGetAll(keys, deserializeBinary, needVer, recovery, readRepairStrategy);
         }
     }
 
@@ -4936,7 +4944,7 @@
      * @param deserializeBinary Deserialize binary flag.
      * @param needVer Need version.
      * @param recovery Recovery flag.
-     * @param readRepair Read repair flag.
+     * @param readRepairStrategy Read repair strategy.
      * @return Map of cached values.
      * @throws IgniteCheckedException If read failed.
      */
@@ -4945,7 +4953,7 @@
         boolean deserializeBinary,
         boolean needVer,
         boolean recovery,
-        boolean readRepair) throws IgniteCheckedException {
+        ReadRepairStrategy readRepairStrategy) throws IgniteCheckedException {
         checkJta();
 
         return getAllAsync(keys,
@@ -4954,7 +4962,7 @@
             ctx.kernalContext().job().currentTaskName(),
             deserializeBinary,
             recovery,
-            readRepair,
+            readRepairStrategy,
             /*skip vals*/false,
             needVer).get();
     }
@@ -4978,7 +4986,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         boolean skipVals,
         final boolean needVer
     ) {
@@ -4989,16 +4997,16 @@
             taskName,
             deserializeBinary,
             recovery,
-            readRepair,
+            readRepairStrategy,
             skipVals,
             needVer);
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             CacheOperationContext opCtx = ctx.operationContextPerCall();
 
             return getWithRepairAsync(
                 fut,
-                () -> repairAsync(keys, opCtx, skipVals),
+                (ks) -> repairAsync(ks, opCtx, skipVals),
                 () -> repairableGetAllAsync(
                     keys,
                     forcePrimary,
@@ -5006,7 +5014,7 @@
                     taskName,
                     deserializeBinary,
                     recovery,
-                    readRepair,
+                    readRepairStrategy,
                     skipVals,
                     needVer));
         }
@@ -5022,7 +5030,7 @@
      */
     private <R> IgniteInternalFuture<R> getWithRepairAsync(
         IgniteInternalFuture<R> orig,
-        Supplier<IgniteInternalFuture<Void>> repair,
+        Function<Collection<KeyCacheObject>, IgniteInternalFuture<Void>> repair,
         Supplier<IgniteInternalFuture<R>> retry) {
         final GridNearTxLocal tx = checkCurrentTx();
         final CacheOperationContext opCtx = ctx.operationContextPerCall();
@@ -5034,7 +5042,7 @@
                 fut.onDone(f.get());
             }
             catch (IgniteConsistencyViolationException e1) {
-                repair.get().listen((repFut) -> {
+                repair.apply(e1.keys()).listen((repFut) -> {
                     if (repFut.error() != null)
                         fut.onDone(repFut.error());
                     else {
@@ -5072,13 +5080,13 @@
      * @param skipVals Skip values flag.
      * @return Compound future that represents a result of repair action.
      */
-    protected IgniteInternalFuture<Void> repairAsync(
-        Collection<? extends K> keys,
+    private IgniteInternalFuture<Void> repairAsync(
+        Collection<KeyCacheObject> keys,
         final CacheOperationContext opCtx,
         boolean skipVals) {
-        GridCompoundFuture fut = new GridCompoundFuture();
+        GridCompoundReadRepairFuture fut = new GridCompoundReadRepairFuture();
 
-        for (K key : keys)
+        for (KeyCacheObject key : keys)
             fut.add(repairAsync(key, opCtx, skipVals));
 
         fut.markInitialized();
@@ -5094,16 +5102,17 @@
      * @param skipVals Skip values flag.
      * @return Recover future.
      */
-    protected IgniteInternalFuture<Void> repairAsync(
-        final K key,
+    private IgniteInternalFuture<Void> repairAsync(
+        final KeyCacheObject key,
         final CacheOperationContext opCtx,
         boolean skipVals) {
         assert ctx.transactional();
 
         final GridNearTxLocal orig = checkCurrentTx();
 
-        // Pessimistic non-read-committed 'get' should be fixed inside its own tx, the only exception is 'contains'.
-        assert orig == null || orig.optimistic() || orig.readCommitted() || /*contains*/ skipVals;
+        assert orig == null || orig.optimistic() || orig.readCommitted() || /*contains*/ skipVals :
+            "Pessimistic non-read-committed 'get' should be fixed inside its own tx, the only exception is 'contains' " +
+                "[tx=" + orig + ", skipVals=" + skipVals + "]";
 
         // Async check and recover if necessary.
         return ctx.kernalContext().closure().callLocalSafe(new GridPlainCallable<Void>() {
@@ -5113,7 +5122,7 @@
                 ctx.operationContextPerCall(opCtx);
 
                 try (Transaction tx = ctx.grid().transactions().txStart(PESSIMISTIC, SERIALIZABLE)) {
-                    get(key); // Repair.
+                    get((K)key, null, false, false); // Repair.
 
                     final GridNearTxLocal tx0 = checkCurrentTx();
 
@@ -5700,10 +5709,9 @@
 
         /**
          * @param tx Transaction.
-         * @param opCtx Operation context.
          * @return Operation future.
          */
-        public IgniteInternalFuture<T> op(final GridNearTxLocal tx, CacheOperationContext opCtx) {
+        public IgniteInternalFuture<T> op(final GridNearTxLocal tx) {
             AffinityTopologyVersion txTopVer = tx.topologyVersionSnapshot();
 
             if (txTopVer != null)
@@ -5717,7 +5725,7 @@
             if (topFut == null || topFut.isDone())
                 return op(tx, topVer);
             else
-                return waitTopologyFuture(topFut, topVer, tx, opCtx);
+                return waitTopologyFuture(topFut, topVer, tx, ctx.operationContextPerCall());
         }
 
         /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheContext.java
index cbb101a..670b5a6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheContext.java
@@ -1654,6 +1654,11 @@
         return conflictRslvr != null;
     }
 
+    /** @return Conflict resolver. */
+    public CacheVersionConflictResolver conflictResolver() {
+        return conflictRslvr;
+    }
+
     /**
      * Resolve DR conflict.
      *
@@ -1766,7 +1771,7 @@
      * @param keepBinary Keep binary flag.
      * @return Unwrapped collection.
      */
-    public Collection<Object> unwrapBinariesIfNeeded(Collection<Object> col, boolean keepBinary) {
+    public Collection<Object> unwrapBinariesIfNeeded(Collection<?> col, boolean keepBinary) {
         return CacheObjectUtils.unwrapBinariesIfNeeded(cacheObjCtx, col, keepBinary);
     }
 
@@ -2140,7 +2145,8 @@
      * @return {@code True} if it is possible to directly read offheap instead of using {@link GridCacheEntryEx#innerGet}.
      */
     public boolean readNoEntry(@Nullable IgniteCacheExpiryPolicy expiryPlc, boolean readers) {
-        return mvccEnabled() || (!config().isOnheapCacheEnabled() && !readers && expiryPlc == null);
+        return mvccEnabled()
+                || (!config().isOnheapCacheEnabled() && !readers && expiryPlc == null && config().getPlatformCacheConfiguration() == null);
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
index 0bcc1f0..f2edcd4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
@@ -758,7 +758,8 @@
 
             CacheObject val;
 
-            boolean valid = valid(tx != null ? tx.topologyVersion() : cctx.affinity().affinityTopologyVersion());
+            AffinityTopologyVersion topVer = tx != null ? tx.topologyVersion() : cctx.affinity().affinityTopologyVersion();
+            boolean valid = valid(topVer);
 
             if (valid) {
                 val = this.val;
@@ -768,6 +769,9 @@
                         unswap(null, false);
 
                         val = this.val;
+
+                        if (val != null && tx == null)
+                            updatePlatformCache(val, topVer);
                     }
                 }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCachePartitionExchangeManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCachePartitionExchangeManager.java
index 6c19ec7..5ca5144 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCachePartitionExchangeManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCachePartitionExchangeManager.java
@@ -3300,7 +3300,7 @@
 
                             assert fut.changedAffinity() :
                                 "Reassignment request started for exchange future which didn't change affinity " +
-                                    "[exchId=" + exchId + ", fut=" + exchFut + ']';
+                                    "[exchId=" + exchId + ", fut=" + fut + ']';
 
                             if (fut.hasInapplicableNodesForRebalance()) {
                                 GridDhtPartitionsExchangeFuture lastFut = lastFinishedFut.get();
@@ -3311,15 +3311,11 @@
                                 if (fut.topologyVersion().equals(lastAffChangedVer))
                                     exchFut = fut;
                                 else if (lastAffChangedVer.after(exchId.topologyVersion())) {
-                                    // There is a new exchange which should trigger rebalancing.
-                                    // This reassignment request can be skipped.
-                                    if (log.isInfoEnabled()) {
-                                        log.info("Partitions reassignment request skipped due to affinity was already changed" +
-                                            " [reassignTopVer=" + exchId.topologyVersion() +
-                                            ", lastAffChangedTopVer=" + lastAffChangedVer + ']');
-                                    }
+                                    exchId = lastFut.exchangeId();
 
-                                    continue;
+                                    exchFut = lastFut;
+
+                                    exchFut.copyInapplicableNodesFrom(fut);
                                 }
                             }
                         }
@@ -3519,13 +3515,14 @@
 
                                 long rebId = cnt;
 
+                                final GridDhtPartitionExchangeId finalExchId = exchId;
                                 rebFut.listen(new IgniteInClosure<IgniteInternalFuture<Boolean>>() {
                                     @Override public void apply(IgniteInternalFuture<Boolean> f) {
                                         U.log(log, "Rebalancing scheduled [order=" + rebList +
                                             ", top=" + finalR.topologyVersion() +
                                             ", rebalanceId=" + rebId +
-                                            ", evt=" + exchId.discoveryEventName() +
-                                            ", node=" + exchId.nodeId() + ']');
+                                            ", evt=" + finalExchId.discoveryEventName() +
+                                            ", node=" + finalExchId.nodeId() + ']');
 
                                         finalR.requestPartitions();
                                     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheProxyImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheProxyImpl.java
index 0e2ff38..3d66277 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheProxyImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheProxyImpl.java
@@ -294,7 +294,7 @@
                         false,
                         null,
                         false,
-                        false,
+                        null,
                         DFLT_ALLOW_ATOMIC_OPS_IN_TX));
         }
         finally {
@@ -316,7 +316,7 @@
                     false,
                     null,
                     false,
-                    false,
+                    null,
                     DFLT_ALLOW_ATOMIC_OPS_IN_TX));
     }
 
@@ -1566,7 +1566,7 @@
                         false,
                         null,
                         false,
-                        false,
+                        null,
                         DFLT_ALLOW_ATOMIC_OPS_IN_TX));
         }
         finally {
@@ -1587,7 +1587,7 @@
                     true,
                     null,
                     false,
-                    false,
+                    null,
                     DFLT_ALLOW_ATOMIC_OPS_IN_TX));
         }
         finally {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java
index e607f17..a962b24 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java
@@ -56,6 +56,7 @@
 import org.apache.ignite.cache.CacheManager;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.query.AbstractContinuousQuery;
 import org.apache.ignite.cache.query.ContinuousQuery;
 import org.apache.ignite.cache.query.ContinuousQueryWithTransformer;
@@ -369,7 +370,7 @@
     }
 
     /** {@inheritDoc} */
-    @Override public IgniteCache<K, V> withReadRepair() {
+    @Override public IgniteCache<K, V> withReadRepair(ReadRepairStrategy strategy) {
         throw new UnsupportedOperationException();
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java
index d70df6e..e636d4f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/CacheObjectBinaryProcessorImpl.java
@@ -60,8 +60,10 @@
 import org.apache.ignite.internal.IgniteNodeAttributes;
 import org.apache.ignite.internal.NodeStoppingException;
 import org.apache.ignite.internal.UnregisteredBinaryTypeException;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryClassDescriptor;
 import org.apache.ignite.internal.binary.BinaryContext;
+import org.apache.ignite.internal.binary.BinaryEnumArray;
 import org.apache.ignite.internal.binary.BinaryEnumObjectImpl;
 import org.apache.ignite.internal.binary.BinaryFieldMetadata;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
@@ -97,7 +99,6 @@
 import org.apache.ignite.internal.processors.cacheobject.UserKeyCacheObjectImpl;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.GridUnsafe;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.MutableSingletonList;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
 import org.apache.ignite.internal.util.lang.GridMapEntry;
@@ -325,7 +326,7 @@
 
             transport = new BinaryMetadataTransport(metadataLocCache, metadataFileStore, binaryCtx, ctx, log);
 
-            IgniteUtils.invoke(BinaryMarshaller.class, bMarsh0, "setBinaryContext", binaryCtx, ctx.config());
+            bMarsh0.setBinaryContext(binaryCtx, ctx.config());
 
             binaryMarsh = new GridBinaryMarshaller(binaryCtx);
 
@@ -500,7 +501,36 @@
             for (int i = 0; i < arr.length; i++)
                 pArr[i] = marshalToBinary(arr[i], failIfUnregistered);
 
-            return pArr;
+            if (!BinaryArray.useBinaryArrays())
+                return pArr;
+
+            Class<?> compCls = obj.getClass().getComponentType();
+
+            boolean isBinaryArr = BinaryObject.class.isAssignableFrom(compCls);
+
+            String compClsName = isBinaryArr ? Object.class.getName() : compCls.getName();
+
+            // In case of interface or multidimensional array rely on class name.
+            // Interfaces and array not registered as binary types.
+            BinaryClassDescriptor desc = binaryCtx.descriptorForClass(compCls);
+
+            if (compCls.isEnum() || compCls == BinaryEnumObjectImpl.class) {
+                return new BinaryEnumArray(
+                    binaryCtx,
+                    desc.registered() ? desc.typeId() : GridBinaryMarshaller.UNREGISTERED_TYPE_ID,
+                    compClsName,
+                    pArr
+                );
+            }
+            else {
+                return new BinaryArray(
+                    binaryCtx,
+                    desc.registered() ? desc.typeId() : GridBinaryMarshaller.UNREGISTERED_TYPE_ID,
+                    compClsName,
+                    pArr
+                );
+            }
+
         }
 
         if (obj instanceof IgniteBiTuple) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
index 56550d1..f6744e8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
@@ -31,6 +31,7 @@
 import javax.cache.expiry.ExpiryPolicy;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.events.DiscoveryEvent;
 import org.apache.ignite.events.Event;
@@ -659,7 +660,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         boolean skipVals,
         boolean needVer
     ) {
@@ -672,7 +673,7 @@
             taskName,
             deserializeBinary,
             opCtx != null && opCtx.recovery(),
-            readRepair,
+            readRepairStrategy,
             forcePrimary,
             null,
             skipVals,
@@ -711,7 +712,7 @@
             skipVals,
             /*keep cache objects*/true,
             recovery,
-            false,
+            null,
             /*need version*/true,
             txLbl,
             mvccSnapshot);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTopologyFutureAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTopologyFutureAdapter.java
index 3f637c52..1f248a4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTopologyFutureAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTopologyFutureAdapter.java
@@ -62,10 +62,13 @@
         boolean valid = true;
 
         if (!grp.systemCache()) {
-            TopologyValidator validator = grp.topologyValidator();
+            for (TopologyValidator validator : grp.topologyValidators()) {
+                if (!validator.validate(topNodes)) {
+                    valid = false;
 
-            if (validator != null)
-                valid = validator.validate(topNodes);
+                    break;
+                }
+            }
         }
 
         return new CacheGroupValidation(valid, lostParts);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
index c7a24b2..2a73bc3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
@@ -37,6 +37,7 @@
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.binary.BinaryInvalidTypeException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.NodeStoppingException;
@@ -83,7 +84,6 @@
 import org.apache.ignite.internal.processors.cache.distributed.near.GridNearSingleGetRequest;
 import org.apache.ignite.internal.processors.cache.distributed.near.GridNearSingleGetResponse;
 import org.apache.ignite.internal.processors.cache.distributed.near.consistency.GridNearReadRepairCheckOnlyFuture;
-import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteConsistencyViolationException;
 import org.apache.ignite.internal.processors.cache.dr.GridCacheDrExpirationInfo;
 import org.apache.ignite.internal.processors.cache.dr.GridCacheDrInfo;
 import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
@@ -474,7 +474,7 @@
         final ExpiryPolicy expiryPlc = skipVals ? null : opCtx != null ? opCtx.expiry() : null;
         final boolean skipStore = opCtx != null && opCtx.skipStore();
         final boolean recovery = opCtx != null && opCtx.recovery();
-        final boolean readRepair = opCtx != null && opCtx.readRepair();
+        final ReadRepairStrategy readRepairStrategy = opCtx != null ? opCtx.readRepairStrategy() : null;
 
         return asyncOp(new CO<IgniteInternalFuture<V>>() {
             @Override public IgniteInternalFuture<V> apply() {
@@ -483,7 +483,7 @@
                     taskName,
                     deserializeBinary,
                     recovery,
-                    readRepair,
+                    readRepairStrategy,
                     expiryPlc,
                     skipVals,
                     skipStore,
@@ -498,13 +498,13 @@
         boolean deserializeBinary,
         boolean needVer,
         boolean recovery,
-        boolean readRepair) throws IgniteCheckedException {
+        ReadRepairStrategy readRepairStrategy) throws IgniteCheckedException {
         return getAllAsyncInternal(keys,
             !ctx.config().isReadFromBackup(),
             ctx.kernalContext().job().currentTaskName(),
             deserializeBinary,
             recovery,
-            readRepair,
+            readRepairStrategy,
             false,
             needVer,
             false).get();
@@ -518,7 +518,7 @@
         final String taskName,
         final boolean deserializeBinary,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean skipVals,
         final boolean needVer
     ) {
@@ -527,7 +527,7 @@
             taskName,
             deserializeBinary,
             recovery,
-            readRepair,
+            readRepairStrategy,
             skipVals,
             needVer,
             true);
@@ -538,7 +538,7 @@
      * @param forcePrimary Force primary flag.
      * @param taskName Task name.
      * @param deserializeBinary Deserialize binary flag.
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @param skipVals Skip values flag.
      * @param needVer Need version flag.
      * @param asyncOp Async operation flag.
@@ -550,7 +550,7 @@
         final String taskName,
         final boolean deserializeBinary,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean skipVals,
         final boolean needVer,
         boolean asyncOp
@@ -576,7 +576,7 @@
                         taskName,
                         deserializeBinary,
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         expiryPlc,
                         skipVals,
                         skipStore,
@@ -590,7 +590,7 @@
                 taskName,
                 deserializeBinary,
                 recovery,
-                readRepair,
+                readRepairStrategy,
                 expiryPlc,
                 skipVals,
                 skipStore,
@@ -1374,7 +1374,7 @@
      * @param forcePrimary Force primary flag.
      * @param taskName Task name.
      * @param deserializeBinary Deserialize binary flag.
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @param expiryPlc Expiry policy.
      * @param skipVals Skip values flag.
      * @param skipStore Skip store flag.
@@ -1386,7 +1386,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         @Nullable ExpiryPolicy expiryPlc,
         boolean skipVals,
         boolean skipStore,
@@ -1396,10 +1396,11 @@
 
         IgniteCacheExpiryPolicy expiry = skipVals ? null : expiryPolicy(expiryPlc);
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             return new GridNearReadRepairCheckOnlyFuture(
                 ctx,
                 Collections.singleton(ctx.toCacheKeyObject(key)),
+                readRepairStrategy,
                 !skipStore,
                 taskName,
                 deserializeBinary,
@@ -1449,7 +1450,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         @Nullable ExpiryPolicy expiryPlc,
         boolean skipVals,
         boolean skipStore,
@@ -1461,10 +1462,11 @@
 
         final boolean evt = !skipVals;
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             return new GridNearReadRepairCheckOnlyFuture(
                 ctx,
                 ctx.cacheKeysView(keys),
+                readRepairStrategy,
                 !skipStore,
                 taskName,
                 deserializeBinary,
@@ -1712,20 +1714,6 @@
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected IgniteInternalFuture<Void> repairAsync(Collection<? extends K> keys,
-        final CacheOperationContext opCtx, boolean skipVals) {
-        return new GridFinishedFuture<>(
-            new IgniteConsistencyViolationException("Consistency violation detected during Read Repair procedure."));
-    }
-
-    /** {@inheritDoc} */
-    @Override protected IgniteInternalFuture<Void> repairAsync(final K key, final CacheOperationContext opCtx,
-        boolean skipVals) {
-        return new GridFinishedFuture<>(
-            new IgniteConsistencyViolationException("Consistency violation detected during Read Repair procedure."));
-    }
-
     /**
      * @param nodeId Node ID.
      * @param req Update request.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/colocated/GridDhtColocatedCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/colocated/GridDhtColocatedCache.java
index a120ea8..6af3fda 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/colocated/GridDhtColocatedCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/colocated/GridDhtColocatedCache.java
@@ -25,6 +25,7 @@
 import java.util.Map;
 import java.util.UUID;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
@@ -191,7 +192,7 @@
         final CacheOperationContext opCtx = ctx.operationContextPerCall();
 
         final boolean recovery = opCtx != null && opCtx.recovery();
-        final boolean readRepair = opCtx != null && opCtx.readRepair();
+        final ReadRepairStrategy readRepairStrategy = opCtx != null ? opCtx.readRepairStrategy() : null;
 
         // Get operation bypass Tx in Mvcc mode.
         if (!ctx.mvccEnabled() && tx != null && !tx.implicit() && !skipTx) {
@@ -205,7 +206,7 @@
                         false,
                         opCtx != null && opCtx.skipStore(),
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         needVer);
 
                     return fut.chain(new CX1<IgniteInternalFuture<Map<Object, Object>>, V>() {
@@ -257,10 +258,11 @@
         else
             topVer = ctx.affinity().affinityTopologyVersion();
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             return new GridNearReadRepairCheckOnlyFuture(
                 ctx,
                 Collections.singleton(ctx.toCacheKeyObject(key)),
+                readRepairStrategy,
                 opCtx == null || !opCtx.skipStore(),
                 taskName,
                 deserializeBinary,
@@ -311,7 +313,7 @@
         String taskName,
         final boolean deserializeBinary,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean skipVals,
         final boolean needVer
     ) {
@@ -339,7 +341,7 @@
                         false,
                         opCtx != null && opCtx.skipStore(),
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         needVer);
                 }
             }, opCtx, /*retry*/false);
@@ -374,10 +376,11 @@
         else
             topVer = ctx.affinity().affinityTopologyVersion();
 
-        if (readRepair) {
+        if (readRepairStrategy != null) {
             return new GridNearReadRepairCheckOnlyFuture(
                 ctx,
                 ctx.cacheKeysView(keys),
+                readRepairStrategy,
                 opCtx == null || !opCtx.skipStore(),
                 taskName,
                 deserializeBinary,
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtPartitionsExchangeFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtPartitionsExchangeFuture.java
index 7e70d7b..4ef0d91 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtPartitionsExchangeFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtPartitionsExchangeFuture.java
@@ -605,6 +605,19 @@
     }
 
     /**
+     * Marks nodes as not applicable for full and historical rebalancing.
+     *
+     * @param fut Exchange future that is used for getting nodes that are not applicable for rebalancing.
+     */
+    public void copyInapplicableNodesFrom(GridDhtPartitionsExchangeFuture fut) {
+        fut.exclusionsFromFullRebalance.forEach((k, v) -> {
+            v.forEach(p -> markNodeAsInapplicableForFullRebalance(k.get2(), k.get1(), p));
+        });
+
+        fut.exclusionsFromHistoricalRebalance.forEach(this::markNodeAsInapplicableForHistoricalRebalance);
+    }
+
+    /**
      * Marks the given node as not applicable for full rebalancing
      * for the given group and partition.
      *
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
index 680eed2..3845152 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
@@ -596,7 +596,8 @@
 
                 long updateSeq = this.updateSeq.incrementAndGet();
 
-                cntrMap.clear();
+                if (exchFut.exchangeType() == ALL && !exchFut.rebalanced())
+                    cntrMap.clear();
 
                 initializeFullMap(updateSeq);
 
@@ -781,7 +782,6 @@
         ctx.database().checkpointReadLock();
 
         try {
-
             lock.writeLock().lock();
 
             try {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearAtomicCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearAtomicCache.java
index 114f87f..03099c3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearAtomicCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearAtomicCache.java
@@ -29,6 +29,7 @@
 import javax.cache.processor.EntryProcessorException;
 import javax.cache.processor.EntryProcessorResult;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.CacheEntryPredicate;
@@ -414,7 +415,7 @@
         String taskName,
         boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         boolean skipVals,
         boolean needVer
     ) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTransactionalCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTransactionalCache.java
index 389d109..5887b8d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTransactionalCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTransactionalCache.java
@@ -25,6 +25,7 @@
 import java.util.Map;
 import java.util.UUID;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
@@ -121,7 +122,7 @@
         String taskName,
         final boolean deserializeBinary,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean skipVals,
         final boolean needVer
     ) {
@@ -149,7 +150,7 @@
                         false,
                         skipStore,
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         needVer);
                 }
             }, opCtx, /*retry*/false);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxLocal.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxLocal.java
index 332bb3b..733ec86 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxLocal.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxLocal.java
@@ -36,6 +36,7 @@
 import javax.cache.expiry.ExpiryPolicy;
 import javax.cache.processor.EntryProcessor;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.NodeStoppingException;
@@ -2225,7 +2226,7 @@
      * @param skipVals Skip values flag.
      * @param keepCacheObjects Keep cache objects
      * @param skipStore Skip store flag.
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @return Future for this get.
      */
     @SuppressWarnings("unchecked")
@@ -2238,7 +2239,7 @@
         final boolean keepCacheObjects,
         final boolean skipStore,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean needVer) {
         if (F.isEmpty(keys))
             return new GridFinishedFuture<>(Collections.<K, V>emptyMap());
@@ -2283,7 +2284,7 @@
                     keepCacheObjects,
                     skipStore,
                     recovery,
-                    readRepair,
+                    readRepairStrategy,
                     needVer);
             }
             catch (IgniteCheckedException e) {
@@ -2448,16 +2449,17 @@
                                 keepCacheObjects,
                                 skipStore,
                                 recovery,
-                                readRepair,
+                                readRepairStrategy,
                                 needVer,
                                 expiryPlc0);
                         }
 
-                        if (readRepair) {
+                        if (readRepairStrategy != null) { // Checking and repairing each locked entry (if necessary).
                             return new GridNearReadRepairFuture(
                                 topVer != null ? topVer : topologyVersion(),
                                 cacheCtx,
                                 keys,
+                                readRepairStrategy,
                                 !skipStore,
                                 taskName,
                                 deserializeBinary,
@@ -2469,12 +2471,13 @@
                                             // For every fixed entry.
                                             for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fut.get().entrySet()) {
                                                 EntryGetResult getRes = entry.getValue();
+                                                KeyCacheObject key = entry.getKey();
 
-                                                enlistWrite(
+                                                enlistWrite( // Fixing inconsistency on commit.
                                                     cacheCtx,
                                                     entryTopVer,
-                                                    entry.getKey(),
-                                                    getRes.value(),
+                                                    key,
+                                                    getRes != null ? getRes.value() : null,
                                                     expiryPlc0,
                                                     null,
                                                     null,
@@ -2489,19 +2492,25 @@
                                                     null);
 
                                                 // Rewriting fixed, initially filled by explicit lock operation.
-                                                cacheCtx.addResult(retMap,
-                                                    entry.getKey(),
-                                                    getRes.value(),
-                                                    skipVals,
-                                                    keepCacheObjects,
-                                                    deserializeBinary,
-                                                    false,
-                                                    getRes,
-                                                    getRes.version(),
-                                                    0,
-                                                    0,
-                                                    needVer,
-                                                    U.deploymentClassLoader(cctx.kernalContext(), deploymentLdrId));
+                                                if (getRes != null)
+                                                    cacheCtx.addResult(retMap,
+                                                        key,
+                                                        getRes.value(),
+                                                        skipVals,
+                                                        keepCacheObjects,
+                                                        deserializeBinary,
+                                                        false,
+                                                        getRes,
+                                                        getRes.version(),
+                                                        0,
+                                                        0,
+                                                        needVer,
+                                                        U.deploymentClassLoader(cctx.kernalContext(), deploymentLdrId));
+                                                else
+                                                    retMap.remove(keepCacheObjects ? key :
+                                                        cacheCtx.cacheObjectContext().unwrapBinaryIfNeeded(
+                                                            key, !deserializeBinary, false,
+                                                            U.deploymentClassLoader(cctx.kernalContext(), deploymentLdrId)));
                                             }
 
                                             return Collections.emptyMap();
@@ -2585,7 +2594,7 @@
                         keepCacheObjects,
                         skipStore,
                         recovery,
-                        readRepair,
+                        readRepairStrategy,
                         needVer,
                         expiryPlc);
                 }
@@ -2629,7 +2638,7 @@
         boolean keepCacheObjects,
         boolean skipStore,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         final boolean needVer
     ) throws IgniteCheckedException {
         assert !F.isEmpty(keys);
@@ -2802,7 +2811,7 @@
                             GridCacheVersion readVer = null;
                             EntryGetResult getRes = null;
 
-                        if ((!pessimistic() || (readCommitted() && !skipVals)) && !readRepair) {
+                        if ((!pessimistic() || (readCommitted() && !skipVals)) && readRepairStrategy == null) {
                             IgniteCacheExpiryPolicy accessPlc =
                                 optimistic() ? accessPolicy(cacheCtx, txKey, expiryPlc) : null;
 
@@ -3024,7 +3033,7 @@
             needReadVer,
             keepBinary,
             recovery,
-            false,
+            null,
             expiryPlc,
             c);
     }
@@ -3126,7 +3135,7 @@
      * @param skipVals Skip values flag.
      * @param needVer If {@code true} version is required for loaded values.
      * @param c Closure to be applied for loaded values.
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @param expiryPlc Expiry policy.
      * @return Future with {@code True} value if loading took place.
      */
@@ -3140,7 +3149,7 @@
         final boolean needVer,
         boolean keepBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         final ExpiryPolicy expiryPlc,
         final GridInClosure3<KeyCacheObject, Object, GridCacheVersion> c
     ) {
@@ -3176,10 +3185,11 @@
             });
         }
         else if (cacheCtx.isColocated()) {
-            if (readRepair) {
+            if (readRepairStrategy != null) {
                 return new GridNearReadRepairCheckOnlyFuture(
                     cacheCtx,
                     keys,
+                    readRepairStrategy,
                     readThrough,
                     taskName,
                     false,
@@ -4894,7 +4904,7 @@
      * @param skipVals Skip values flag.
      * @param keepCacheObjects Keep cache objects flag.
      * @param skipStore Skip store flag.
-     * @param readRepair Read Repair flag.
+     * @param readRepairStrategy Read Repair strategy.
      * @param expiryPlc Expiry policy.
      * @return Loaded key-value pairs.
      */
@@ -4908,7 +4918,7 @@
         final boolean keepCacheObjects,
         final boolean skipStore,
         final boolean recovery,
-        final boolean readRepair,
+        final ReadRepairStrategy readRepairStrategy,
         final boolean needVer,
         final ExpiryPolicy expiryPlc
     ) {
@@ -4945,7 +4955,7 @@
                 needReadVer,
                 !deserializeBinary,
                 recovery,
-                readRepair,
+                readRepairStrategy,
                 expiryPlc,
                 new GridInClosure3<KeyCacheObject, Object, GridCacheVersion>() {
                     @Override public void apply(KeyCacheObject key, Object val, GridCacheVersion loadVer) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxQueryEnlistRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxQueryEnlistRequest.java
index 549afc1..b8f3a57 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxQueryEnlistRequest.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxQueryEnlistRequest.java
@@ -20,6 +20,8 @@
 import java.nio.ByteBuffer;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.GridDirectTransient;
+import org.apache.ignite.internal.binary.BinaryMarshaller;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.GridCacheIdMessage;
 import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
@@ -28,6 +30,7 @@
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.plugin.extensions.communication.MessageReader;
 import org.apache.ignite.plugin.extensions.communication.MessageWriter;
 
@@ -298,7 +301,15 @@
     @Override public void finishUnmarshal(GridCacheSharedContext ctx, ClassLoader ldr) throws IgniteCheckedException {
         super.finishUnmarshal(ctx, ldr);
 
-        if (paramsBytes != null && params == null)
+        if (paramsBytes == null || params != null)
+            return;
+
+        Marshaller m = ctx.kernalContext().config().getMarshaller();
+
+        if (m instanceof BinaryMarshaller)
+            // To avoid deserializing of enum types.
+            params = BinaryUtils.rawArrayFromBinary(((BinaryMarshaller)m).binaryMarshaller().unmarshal(paramsBytes, ldr));
+        else
             params = U.unmarshal(ctx, paramsBytes, ldr);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridCompoundReadRepairFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridCompoundReadRepairFuture.java
new file mode 100644
index 0000000..3eaacdc
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridCompoundReadRepairFuture.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cache.distributed.near.consistency;
+
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.util.future.GridFutureAdapter;
+import org.apache.ignite.lang.IgniteInClosure;
+
+/**
+ * Compound future that represents the result of the external fixes for some keys.
+ */
+public class GridCompoundReadRepairFuture extends GridFutureAdapter<Void> implements IgniteInClosure<IgniteInternalFuture<Void>> {
+    /** Listener calls updater. */
+    private static final AtomicIntegerFieldUpdater<GridCompoundReadRepairFuture> LSNR_CALLS_UPD =
+        AtomicIntegerFieldUpdater.newUpdater(GridCompoundReadRepairFuture.class, "lsnrCalls");
+
+    /** Initialized. */
+    private volatile boolean inited;
+
+    /** Listener calls. */
+    private volatile int lsnrCalls;
+
+    /** Count of compounds in the future. */
+    private volatile int size;
+
+    /** Irreparable Keys. */
+    private volatile Collection<Object> irreparableKeys;
+
+    /**
+     * @param fut Future.
+     */
+    public void add(IgniteInternalFuture<Void> fut) {
+        size++; // All additions are from the same thread.
+
+        fut.listen(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void apply(IgniteInternalFuture<Void> fut) {
+        Throwable e = fut.error();
+
+        if (e != null) {
+            if (e instanceof IgniteIrreparableConsistencyViolationException) {
+                Collection<Object> repairableKey = ((IgniteIrreparableConsistencyViolationException)e).repairableKeys();
+                Collection<Object> irreparableKeys = ((IgniteIrreparableConsistencyViolationException)e).irreparableKeys();
+
+                assert repairableKey == null || repairableKey.isEmpty() : repairableKey.size();
+                assert irreparableKeys.size() == 1 : irreparableKeys.size(); // Single key fix.
+
+                synchronized (this) {
+                    if (this.irreparableKeys == null)
+                        this.irreparableKeys = ConcurrentHashMap.newKeySet();
+                }
+
+                this.irreparableKeys.addAll(irreparableKeys);
+            }
+            else
+                onDone(e);
+        }
+
+        LSNR_CALLS_UPD.incrementAndGet(this);
+
+        checkComplete();
+    }
+
+    /**
+     * Mark this future as initialized.
+     */
+    public final void markInitialized() {
+        inited = true;
+
+        checkComplete();
+    }
+
+    /**
+     * Check completeness of the future.
+     */
+    private void checkComplete() {
+        assert lsnrCalls <= size;
+
+        if (inited && !isDone() && lsnrCalls == size) {
+            if (irreparableKeys == null)
+                onDone();
+            else
+                onDone(new IgniteIrreparableConsistencyViolationException(null, irreparableKeys));
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairAbstractFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairAbstractFuture.java
index 61fd137..c0cdb3d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairAbstractFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairAbstractFuture.java
@@ -17,7 +17,9 @@
 
 package org.apache.ignite.internal.processors.cache.distributed.near.consistency;
 
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -25,7 +27,9 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cache.CacheEntryVersion;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.events.CacheConsistencyViolationEvent;
 import org.apache.ignite.internal.IgniteInternalFuture;
@@ -33,6 +37,7 @@
 import org.apache.ignite.internal.cluster.ClusterTopologyServerNotFoundException;
 import org.apache.ignite.internal.managers.eventstorage.GridEventStorageManager;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.EntryGetResult;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.IgniteCacheExpiryPolicy;
@@ -88,13 +93,16 @@
     protected final IgniteInternalTx tx;
 
     /** Primaries per key. */
-    private volatile Map<KeyCacheObject, ClusterNode> primaries;
+    protected volatile Map<KeyCacheObject, ClusterNode> primaries;
+
+    /** Strategy. */
+    protected final ReadRepairStrategy strategy;
 
     /** Remap flag. */
     private final boolean canRemap;
 
     /** Latest mapped topology version. */
-    private AffinityTopologyVersion topVer;
+    private volatile AffinityTopologyVersion topVer;
 
     /**
      * Creates a new instance of GridNearReadRepairAbstractFuture.
@@ -102,6 +110,7 @@
      * @param topVer Topology version.
      * @param ctx Cache context.
      * @param keys Keys.
+     * @param strategy Read repair strategy.
      * @param readThrough Read-through flag.
      * @param taskName Task name.
      * @param deserializeBinary Deserialize binary flag.
@@ -113,6 +122,7 @@
         AffinityTopologyVersion topVer,
         GridCacheContext<KeyCacheObject, EntryGetResult> ctx,
         Collection<KeyCacheObject> keys,
+        ReadRepairStrategy strategy,
         boolean readThrough,
         String taskName,
         boolean deserializeBinary,
@@ -128,17 +138,26 @@
         this.expiryPlc = expiryPlc;
         this.tx = tx;
 
+        assert strategy != null;
+
+        this.strategy = strategy;
+
         canRemap = topVer == null;
 
-        map(canRemap ? ctx.affinity().affinityTopologyVersion() : topVer);
+        this.topVer = canRemap ? ctx.affinity().affinityTopologyVersion() : topVer;
     }
 
     /**
-     * @param topVer Affinity topology version.
+     *
      */
-    protected synchronized void map(AffinityTopologyVersion topVer) {
-        this.topVer = topVer;
+    protected final void init() {
+        map();
+    }
 
+    /**
+     *
+     */
+    private synchronized void map() {
         assert futs.isEmpty() : "Remapping started without the clean-up.";
 
         Map<KeyCacheObject, ClusterNode> primaryNodes = new HashMap<>();
@@ -199,10 +218,12 @@
     /**
      * @param topVer Topology version.
      */
-    protected void remap(AffinityTopologyVersion topVer) {
+    protected final void remap(AffinityTopologyVersion topVer) {
         futs.clear();
 
-        map(topVer);
+        this.topVer = topVer;
+
+        map();
     }
 
     /**
@@ -210,7 +231,7 @@
      *
      * @param finished Future represents a result of GET operation.
      */
-    protected synchronized void onResult(IgniteInternalFuture<Map<KeyCacheObject, EntryGetResult>> finished) {
+    protected final synchronized void onResult(IgniteInternalFuture<Map<KeyCacheObject, EntryGetResult>> finished) {
         if (isDone() // All subfutures (including currently processing) were successfully finished at previous future processing.
             || (topVer == null) // Remapping, ignoring any updates until remapped.
             || !futs.containsValue((GridPartitionedGetFuture<KeyCacheObject, EntryGetResult>)finished)) // Remapped.
@@ -255,50 +276,116 @@
     protected abstract void reduce();
 
     /**
+     * Checks consistency.
+     *
+     * @return Regular `get` result when data is consistent.
+     */
+    protected final Map<KeyCacheObject, EntryGetResult> check() throws IgniteCheckedException {
+        Map<KeyCacheObject, EntryGetResult> resMap = new HashMap<>(keys.size());
+        Set<KeyCacheObject> inconsistentKeys = new HashSet<>();
+
+        for (GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut : futs.values()) {
+            for (KeyCacheObject key : fut.keys()) {
+                EntryGetResult curRes = fut.result().get(key);
+
+                if (!resMap.containsKey(key)) {
+                    resMap.put(key, curRes);
+
+                    continue;
+                }
+
+                EntryGetResult prevRes = resMap.get(key);
+
+                if (curRes != null) {
+                    if (prevRes == null || prevRes.version().compareTo(curRes.version()) != 0)
+                        inconsistentKeys.add(key);
+                    else {
+                        CacheObject curVal = curRes.value();
+                        CacheObject prevVal = prevRes.value();
+
+                        byte[] curBytes = curVal.valueBytes(ctx.cacheObjectContext());
+                        byte[] prevBytes = prevVal.valueBytes(ctx.cacheObjectContext());
+
+                        if (!Arrays.equals(curBytes, prevBytes))
+                            inconsistentKeys.add(key);
+
+                    }
+                }
+                else if (prevRes != null)
+                    inconsistentKeys.add(key);
+            }
+        }
+
+        if (!inconsistentKeys.isEmpty())
+            throw new IgniteConsistencyViolationException(inconsistentKeys);
+
+        return resMap;
+    }
+
+    /**
      * @param fixedEntries Fixed map.
      */
-    protected void recordConsistencyViolation(
-        Set<KeyCacheObject> inconsistentKeys,
-        Map<KeyCacheObject, EntryGetResult> fixedEntries
+    protected final void recordConsistencyViolation(
+        Collection<KeyCacheObject> inconsistentKeys,
+        Map<KeyCacheObject, EntryGetResult> fixedEntries,
+        ReadRepairStrategy strategy
     ) {
         GridEventStorageManager evtMgr = ctx.gridEvents();
 
         if (!evtMgr.isRecordable(EVT_CONSISTENCY_VIOLATION))
             return;
 
-        Map<Object, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> originalMap = new HashMap<>();
+        Map<Object, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> entries = new HashMap<>();
 
         for (Map.Entry<ClusterNode, GridPartitionedGetFuture<KeyCacheObject, EntryGetResult>> pair : futs.entrySet()) {
             ClusterNode node = pair.getKey();
 
             GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut = pair.getValue();
 
-            for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fut.result().entrySet()) {
-                KeyCacheObject key = entry.getKey();
-
+            for (KeyCacheObject key : fut.keys()) {
                 if (inconsistentKeys.contains(key)) {
-                    EntryGetResult res = entry.getValue();
-                    CacheEntryVersion ver = res.version();
-
-                    Object val = ctx.unwrapBinaryIfNeeded(res.value(), !deserializeBinary, false, null);
-
                     Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> map =
-                        originalMap.computeIfAbsent(
-                            ctx.unwrapBinaryIfNeeded(key, false, false, null), k -> new HashMap<>());
+                        entries.computeIfAbsent(
+                            ctx.unwrapBinaryIfNeeded(key, !deserializeBinary, false, null), k -> new HashMap<>());
+
+                    EntryGetResult res = fut.result().get(key);
+                    CacheEntryVersion ver = res != null ? res.version() : null;
+
+                    Object val = res != null ? ctx.unwrapBinaryIfNeeded(res.value(), !deserializeBinary, false, null) : null;
 
                     boolean primary = primaries.get(key).equals(fut.affNode());
-                    boolean correct = fixedEntries != null && fixedEntries.get(key).equals(res);
+                    boolean correct = fixedEntries != null &&
+                        ((fixedEntries.get(key) != null && fixedEntries.get(key).equals(res)) ||
+                            (fixedEntries.get(key) == null && res == null));
 
                     map.put(node, new EventEntryInfo(val, ver, primary, correct));
                 }
             }
         }
 
+        Map<Object, Object> fixed;
+
+        if (fixedEntries == null)
+            fixed = Collections.emptyMap();
+        else {
+            fixed = new HashMap<>();
+
+            for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fixedEntries.entrySet()) {
+                Object key = ctx.unwrapBinaryIfNeeded(entry.getKey(), !deserializeBinary, false, null);
+                Object val = entry.getValue() != null ?
+                    ctx.unwrapBinaryIfNeeded(entry.getValue().value(), !deserializeBinary, false, null) : null;
+
+                fixed.put(key, val);
+            }
+        }
+
         evtMgr.record(new CacheConsistencyViolationEvent(
             ctx.name(),
             ctx.discovery().localNode(),
-            "Consistency violation fixed.",
-            originalMap));
+            "Consistency violation was " + (fixed == null ? "NOT " : "") + "fixed.",
+            entries,
+            fixed,
+            strategy));
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairCheckOnlyFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairCheckOnlyFuture.java
index 48d7ce1..5f2b57c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairCheckOnlyFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairCheckOnlyFuture.java
@@ -17,21 +17,16 @@
 
 package org.apache.ignite.internal.processors.cache.distributed.near.consistency;
 
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.IgniteInternalFuture;
-import org.apache.ignite.internal.processors.cache.CacheObjectAdapter;
 import org.apache.ignite.internal.processors.cache.EntryGetResult;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.IgniteCacheExpiryPolicy;
 import org.apache.ignite.internal.processors.cache.KeyCacheObject;
-import org.apache.ignite.internal.processors.cache.distributed.dht.GridPartitionedGetFuture;
 import org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx;
 import org.apache.ignite.internal.util.lang.GridClosureException;
 import org.apache.ignite.internal.util.typedef.F;
@@ -56,6 +51,7 @@
      *
      * @param ctx Cache context.
      * @param keys Keys.
+     * @param strategy Read repair strategy.
      * @param readThrough Read-through flag.
      * @param taskName Task name.
      * @param deserializeBinary Deserialize binary flag.
@@ -69,6 +65,7 @@
     public GridNearReadRepairCheckOnlyFuture(
         GridCacheContext ctx,
         Collection<KeyCacheObject> keys,
+        ReadRepairStrategy strategy,
         boolean readThrough,
         String taskName,
         boolean deserializeBinary,
@@ -81,6 +78,7 @@
         super(null,
             ctx,
             keys,
+            strategy,
             readThrough,
             taskName,
             deserializeBinary,
@@ -91,68 +89,34 @@
         this.skipVals = skipVals;
         this.needVer = needVer;
         this.keepCacheObjects = keepCacheObjects;
+
+        init();
     }
 
     /** {@inheritDoc} */
     @Override protected void reduce() {
-        Map<KeyCacheObject, EntryGetResult> resMap = new HashMap<>(keys.size());
-        Set<KeyCacheObject> inconsistentKeys = new HashSet<>();
-
-        for (GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut : futs.values()) {
-            for (KeyCacheObject key : fut.keys()) {
-                EntryGetResult curRes = fut.result().get(key);
-
-                if (!resMap.containsKey(key)) {
-                    resMap.put(key, curRes);
-
-                    continue;
-                }
-
-                EntryGetResult prevRes = resMap.get(key);
-
-                if (curRes != null) {
-                    if (prevRes == null || prevRes.version().compareTo(curRes.version()) != 0)
-                        inconsistentKeys.add(key);
-                    else {
-                        CacheObjectAdapter curVal = curRes.value();
-                        CacheObjectAdapter prevVal = prevRes.value();
-
-                        try {
-                            byte[] curBytes = curVal.valueBytes(ctx.cacheObjectContext());
-                            byte[] prevBytes = prevVal.valueBytes(ctx.cacheObjectContext());
-
-                            if (!Arrays.equals(curBytes, prevBytes))
-                                inconsistentKeys.add(key);
-                        }
-                        catch (IgniteCheckedException e) {
-                            onDone(e);
-
-                            return;
-                        }
-                    }
-                }
-                else if (prevRes != null)
-                    inconsistentKeys.add(key);
-            }
+        try {
+            onDone(check());
         }
+        catch (IgniteConsistencyViolationException e) {
+            Set<KeyCacheObject> inconsistentKeys = e.keys();
 
-        if (!inconsistentKeys.isEmpty()) {
             if (REMAP_CNT_UPD.incrementAndGet(this) > MAX_REMAP_CNT) {
-                if (!ctx.transactional()) // Will not be fixed, should be recorded as is.
-                    recordConsistencyViolation(inconsistentKeys, /*nothing fixed*/ null);
+                if (ctx.atomic() || strategy == ReadRepairStrategy.CHECK_ONLY) { // Will not be fixed, should be recorded as is.
+                    recordConsistencyViolation(inconsistentKeys, /*nothing fixed*/ null, ReadRepairStrategy.CHECK_ONLY);
 
-                onDone(new IgniteConsistencyViolationException("Distributed cache consistency violation detected."));
+                    onDone(new IgniteIrreparableConsistencyViolationException(null,
+                        ctx.unwrapBinariesIfNeeded(inconsistentKeys, !deserializeBinary)));
+                }
+                else // Should be fixed by concurrent tx(s).
+                    onDone(e);
             }
             else
                 remap(ctx.affinity().affinityTopologyVersion()); // Rechecking possible "false positive" case.
-
-            return;
         }
-
-        // Misses recorded to detect partial misses, but should not be propagated when the key is null at each node.
-        resMap.values().removeIf(Objects::isNull);
-
-        onDone(resMap);
+        catch (IgniteCheckedException e) {
+            onDone(e);
+        }
     }
 
     /**
@@ -165,23 +129,7 @@
             try {
                 final Map<K, V> map = new IgniteBiTuple<>();
 
-                for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fut.get().entrySet()) {
-                    EntryGetResult getRes = entry.getValue();
-
-                    ctx.addResult(map,
-                        entry.getKey(),
-                        getRes.value(),
-                        skipVals,
-                        keepCacheObjects,
-                        deserializeBinary,
-                        false,
-                        getRes,
-                        getRes.version(),
-                        0,
-                        0,
-                        needVer,
-                        null);
-                }
+                addResult(fut, map);
 
                 if (skipVals) {
                     Boolean val = map.isEmpty() ? false : (Boolean)F.firstValue(map);
@@ -207,23 +155,7 @@
             try {
                 final Map<K, V> map = U.newHashMap(keys.size());
 
-                for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fut.get().entrySet()) {
-                    EntryGetResult getRes = entry.getValue();
-
-                    ctx.addResult(map,
-                        entry.getKey(),
-                        getRes.value(),
-                        skipVals,
-                        keepCacheObjects,
-                        deserializeBinary,
-                        false,
-                        getRes,
-                        getRes.version(),
-                        0,
-                        0,
-                        needVer,
-                        null);
-                }
+                addResult(fut, map);
 
                 return map;
             }
@@ -232,4 +164,29 @@
             }
         });
     }
+
+    /**
+     *
+     */
+    private <K, V> void addResult(IgniteInternalFuture<Map<KeyCacheObject, EntryGetResult>> fut,
+        Map<K, V> map) throws IgniteCheckedException {
+        for (Map.Entry<KeyCacheObject, EntryGetResult> entry : fut.get().entrySet()) {
+            EntryGetResult getRes = entry.getValue();
+
+            if (getRes != null)
+                ctx.addResult(map,
+                    entry.getKey(),
+                    getRes.value(),
+                    skipVals,
+                    keepCacheObjects,
+                    deserializeBinary,
+                    false,
+                    getRes,
+                    getRes.version(),
+                    0,
+                    0,
+                    needVer,
+                    U.deploymentClassLoader(ctx.kernalContext(), U.contextDeploymentClassLoaderId(ctx.kernalContext())));
+        }
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairFuture.java
index 1bb064e..2f6c15b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/GridNearReadRepairFuture.java
@@ -19,17 +19,25 @@
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
-import org.apache.ignite.internal.processors.cache.CacheObjectAdapter;
+import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.EntryGetResult;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.IgniteCacheExpiryPolicy;
 import org.apache.ignite.internal.processors.cache.KeyCacheObject;
 import org.apache.ignite.internal.processors.cache.distributed.dht.GridPartitionedGetFuture;
 import org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx;
+import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
+import org.apache.ignite.internal.util.typedef.T2;
+import org.apache.ignite.lang.IgniteBiTuple;
 import org.apache.ignite.transactions.TransactionState;
 
 /**
@@ -43,6 +51,7 @@
      * @param topVer Affinity topology version.
      * @param ctx Cache context.
      * @param keys Keys.
+     * @param strategy Read repair strategy.
      * @param readThrough Read-through flag.
      * @param taskName Task name.
      * @param deserializeBinary Deserialize binary flag.
@@ -54,6 +63,7 @@
         AffinityTopologyVersion topVer,
         GridCacheContext ctx,
         Collection<KeyCacheObject> keys,
+        ReadRepairStrategy strategy,
         boolean readThrough,
         String taskName,
         boolean deserializeBinary,
@@ -63,6 +73,7 @@
         super(topVer,
             ctx,
             keys,
+            strategy,
             readThrough,
             taskName,
             deserializeBinary,
@@ -71,18 +82,81 @@
             tx);
 
         assert ctx.transactional() : "Atomic cache should not be recovered using this future";
+
+        init();
     }
 
     /** {@inheritDoc} */
     @Override protected void reduce() {
-        Map<KeyCacheObject, EntryGetResult> newestMap = new HashMap<>(keys.size()); // Newest entries (by version).
-        Map<KeyCacheObject, EntryGetResult> fixedMap = new HashMap<>(); // Newest entries required to be re-committed.
+        assert strategy != null;
+
+        try {
+            check();
+
+            onDone(Collections.emptyMap()); // Everything is fine.
+        }
+        catch (IgniteConsistencyViolationException e) { // Inconsistent entries found.
+            try {
+                Map<KeyCacheObject, EntryGetResult> fixedMap; // Entries required to be re-committed.
+
+                if (strategy == ReadRepairStrategy.LWW)
+                    fixedMap = fixWithLww(e.keys());
+                else if (strategy == ReadRepairStrategy.PRIMARY)
+                    fixedMap = fixWithPrimary(e.keys());
+                else if (strategy == ReadRepairStrategy.RELATIVE_MAJORITY)
+                    fixedMap = fixWithMajority(e.keys());
+                else if (strategy == ReadRepairStrategy.REMOVE)
+                    fixedMap = fixWithRemove(e.keys());
+                else if (strategy == ReadRepairStrategy.CHECK_ONLY)
+                    throw new IgniteIrreparableConsistencyViolationException(null,
+                        ctx.unwrapBinariesIfNeeded(e.keys(), !deserializeBinary));
+                else
+                    throw new UnsupportedOperationException("Unsupported strategy: " + strategy);
+
+                if (!fixedMap.isEmpty()) {
+                    tx.finishFuture().listen(future -> {
+                        TransactionState state = tx.state();
+
+                        if (state == TransactionState.COMMITTED) // Explicit tx may fix the values but become rolled back later.
+                            recordConsistencyViolation(fixedMap.keySet(), fixedMap, strategy);
+                    });
+                }
+
+                onDone(fixedMap);
+            }
+            catch (IgniteIrreparableConsistencyViolationException ie) { // Unable to repair all entries.
+                recordConsistencyViolation(e.keys(), /*nothing fixed*/ null, strategy);
+
+                onDone(ie);
+            }
+            catch (IgniteCheckedException ce) {
+                onDone(ce);
+            }
+        }
+        catch (IgniteCheckedException e) {
+            onDone(e);
+        }
+    }
+
+    /**
+     *
+     */
+    public Map<KeyCacheObject, EntryGetResult> fixWithLww(Set<KeyCacheObject> inconsistentKeys) throws IgniteCheckedException {
+        Map<KeyCacheObject, EntryGetResult> newestMap = new HashMap<>(inconsistentKeys.size()); // Newest entries (by version).
+        Map<KeyCacheObject, EntryGetResult> fixedMap = new HashMap<>(inconsistentKeys.size());
+
+        Set<KeyCacheObject> irreparableSet = new HashSet<>();
 
         for (GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut : futs.values()) {
-            for (KeyCacheObject key : fut.keys()) {
+            for (KeyCacheObject key : inconsistentKeys) {
+                if (!fut.keys().contains(key))
+                    continue;
+
                 EntryGetResult candidateRes = fut.result().get(key);
 
-                if (!newestMap.containsKey(key)) {
+                boolean hasNewest = newestMap.containsKey(key);
+
+                if (!hasNewest) {
                     newestMap.put(key, candidateRes);
 
                     continue;
@@ -91,9 +165,13 @@
                 EntryGetResult newestRes = newestMap.get(key);
 
                 if (candidateRes != null) {
-                    if (newestRes == null) { // Existing data wins.
-                        newestMap.put(key, candidateRes);
-                        fixedMap.put(key, candidateRes);
+                    if (newestRes == null) {
+                        if (hasNewest) // Newest is null.
+                            irreparableSet.add(key);
+                        else { // Existing data wins.
+                            newestMap.put(key, candidateRes);
+                            fixedMap.put(key, candidateRes);
+                        }
                     }
                     else {
                         int compareRes = candidateRes.version().compareTo(newestRes.version());
@@ -105,40 +183,156 @@
                         else if (compareRes < 0)
                             fixedMap.put(key, newestRes);
                         else if (compareRes == 0) {
-                            CacheObjectAdapter candidateVal = candidateRes.value();
-                            CacheObjectAdapter newestVal = newestRes.value();
+                            CacheObject candidateVal = candidateRes.value();
+                            CacheObject newestVal = newestRes.value();
 
-                            try {
-                                byte[] candidateBytes = candidateVal.valueBytes(ctx.cacheObjectContext());
-                                byte[] newestBytes = newestVal.valueBytes(ctx.cacheObjectContext());
+                            byte[] candidateBytes = candidateVal.valueBytes(ctx.cacheObjectContext());
+                            byte[] newestBytes = newestVal.valueBytes(ctx.cacheObjectContext());
 
-                                if (!Arrays.equals(candidateBytes, newestBytes))
-                                    fixedMap.put(key, newestRes); // Same version, fixing values inconsistency.
-                            }
-                            catch (IgniteCheckedException e) {
-                                onDone(e);
-
-                                return;
-                            }
+                            if (!Arrays.equals(candidateBytes, newestBytes))
+                                irreparableSet.add(key);
                         }
                     }
                 }
                 else if (newestRes != null)
-                    fixedMap.put(key, newestRes); // Existing data wins.
+                    irreparableSet.add(key); // Impossible to detect latest between existing and null.
             }
         }
 
         assert !fixedMap.containsValue(null) : "null should never be considered as a fix";
 
-        if (!fixedMap.isEmpty()) {
-            tx.finishFuture().listen(future -> {
-                TransactionState state = tx.state();
+        if (!irreparableSet.isEmpty())
+            throwIrreparable(inconsistentKeys, irreparableSet);
 
-                if (state == TransactionState.COMMITTED) // Explicit tx may fix the values but become rolled back later.
-                    recordConsistencyViolation(fixedMap.keySet(), fixedMap);
-            });
+        return fixedMap;
+    }
+
+    /**
+     *
+     */
+    public Map<KeyCacheObject, EntryGetResult> fixWithPrimary(Collection<KeyCacheObject> inconsistentKeys) {
+        Map<KeyCacheObject, EntryGetResult> fixedMap = new HashMap<>(inconsistentKeys.size());
+
+        for (GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut : futs.values()) {
+            for (KeyCacheObject key : inconsistentKeys) {
+                if (fut.keys().contains(key) && primaries.get(key).equals(fut.affNode()))
+                    fixedMap.put(key, fut.result().get(key));
+            }
         }
 
-        onDone(fixedMap);
+        return fixedMap;
+    }
+
+    /**
+     *
+     */
+    public Map<KeyCacheObject, EntryGetResult> fixWithRemove(Collection<KeyCacheObject> inconsistentKeys) {
+        Map<KeyCacheObject, EntryGetResult> fixedMap = new HashMap<>(inconsistentKeys.size());
+
+        for (KeyCacheObject key : inconsistentKeys)
+            fixedMap.put(key, null);
+
+        return fixedMap;
+    }
+
+    /**
+     *
+     */
+    public Map<KeyCacheObject, EntryGetResult> fixWithMajority(Collection<KeyCacheObject> inconsistentKeys)
+        throws IgniteCheckedException {
+        /** */
+        class ByteArrayWrapper {
+            final byte[] arr;
+
+            /** */
+            public ByteArrayWrapper(byte[] arr) {
+                this.arr = arr;
+            }
+
+            /** */
+            @Override public boolean equals(Object o) {
+                return Arrays.equals(arr, ((ByteArrayWrapper)o).arr);
+            }
+
+            /** */
+            @Override public int hashCode() {
+                return Arrays.hashCode(arr);
+            }
+        }
+
+        Set<KeyCacheObject> irreparableSet = new HashSet<>(inconsistentKeys.size());
+        Map<KeyCacheObject, EntryGetResult> fixedMap = new HashMap<>(inconsistentKeys.size());
+
+        for (KeyCacheObject key : inconsistentKeys) {
+            Map<T2<ByteArrayWrapper, GridCacheVersion>, T2<EntryGetResult, Integer>> cntMap = new HashMap<>();
+
+            for (GridPartitionedGetFuture<KeyCacheObject, EntryGetResult> fut : futs.values()) {
+                if (!fut.keys().contains(key))
+                    continue;
+
+                EntryGetResult res = fut.result().get(key);
+
+                ByteArrayWrapper wrapped;
+                GridCacheVersion ver;
+
+                if (res != null) {
+                    CacheObject val = res.value();
+
+                    wrapped = new ByteArrayWrapper(val.valueBytes(ctx.cacheObjectContext()));
+                    ver = res.version();
+                }
+                else {
+                    wrapped = new ByteArrayWrapper(null);
+                    ver = null;
+                }
+
+                T2<ByteArrayWrapper, GridCacheVersion> keyVer = new T2<>(wrapped, ver);
+
+                cntMap.putIfAbsent(keyVer, new T2<>(res, 0));
+
+                cntMap.compute(keyVer, (kv, ri) -> new T2<>(ri.getKey(), ri.getValue() + 1));
+            }
+
+            int[] sorted = cntMap.values().stream()
+                .map(IgniteBiTuple::getValue)
+                .sorted(Comparator.reverseOrder())
+                .mapToInt(v -> v)
+                .toArray();
+
+            int max = sorted[0];
+
+            assert max > 0;
+
+            if (sorted.length > 1 && sorted[1] == max) { // Majority was not found.
+                irreparableSet.add(key);
+
+                continue;
+            }
+
+            for (Map.Entry<T2<ByteArrayWrapper, GridCacheVersion>, T2<EntryGetResult, Integer>> count : cntMap.entrySet())
+                if (count.getValue().getValue().equals(max)) {
+                    fixedMap.put(key, count.getValue().getKey());
+
+                    break;
+                }
+        }
+
+        if (!irreparableSet.isEmpty())
+            throwIrreparable(inconsistentKeys, irreparableSet);
+
+        return fixedMap;
+    }
+
+    /**
+     *
+     */
+    private void throwIrreparable(Collection<KeyCacheObject> inconsistentKeys, Set<KeyCacheObject> irreparableSet)
+        throws IgniteIrreparableConsistencyViolationException {
+        Set<KeyCacheObject> repairableKeys = new HashSet<>(inconsistentKeys);
+
+        repairableKeys.removeAll(irreparableSet);
+
+        throw new IgniteIrreparableConsistencyViolationException(ctx.unwrapBinariesIfNeeded(repairableKeys, !deserializeBinary),
+            ctx.unwrapBinariesIfNeeded(irreparableSet, !deserializeBinary));
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteConsistencyViolationException.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteConsistencyViolationException.java
index db3d448..7d102d6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteConsistencyViolationException.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteConsistencyViolationException.java
@@ -17,20 +17,35 @@
 
 package org.apache.ignite.internal.processors.cache.distributed.near.consistency;
 
+import java.util.Set;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.processors.cache.KeyCacheObject;
 
 /**
- * Possible consistency violation exception.
- * Each such case should be rechecked under locks.
+ * Consistency violation exception.
  */
 public class IgniteConsistencyViolationException extends IgniteCheckedException {
     /** */
     private static final long serialVersionUID = 0L;
 
+    /** Inconsistent entries keys. */
+    private final Set<KeyCacheObject> keys;
+
     /**
-     * @param msg Message.
+     * @param keys Keys.
      */
-    public IgniteConsistencyViolationException(String msg) {
-        super(msg);
+    public IgniteConsistencyViolationException(Set<KeyCacheObject> keys) {
+        super("Distributed cache consistency violation detected.");
+
+        assert keys != null && !keys.isEmpty();
+
+        this.keys = keys;
+    }
+
+    /**
+     * Inconsistent entries keys.
+     */
+    public Set<KeyCacheObject> keys() {
+        return keys;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteIrreparableConsistencyViolationException.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteIrreparableConsistencyViolationException.java
new file mode 100644
index 0000000..32ee368
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/consistency/IgniteIrreparableConsistencyViolationException.java
@@ -0,0 +1,63 @@
+/*
+ * 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.cache.distributed.near.consistency;
+
+import java.util.Collection;
+import org.apache.ignite.IgniteCheckedException;
+
+/**
+ * Irreparable consistency violation exception.
+ */
+public class IgniteIrreparableConsistencyViolationException extends IgniteCheckedException {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Repairable keys. */
+    private final Collection<Object> repairableKeys;
+
+    /** Irreparable keys. */
+    private final Collection<Object> irreparableKeys;
+
+    /**
+     * @param repairableKeys  Repairable keys.
+     * @param irreparableKeys Irreparable keys.
+     */
+    public IgniteIrreparableConsistencyViolationException(Collection<Object> repairableKeys,
+        Collection<Object> irreparableKeys) {
+        super("Irreparable distributed cache consistency violation detected.");
+
+        assert irreparableKeys != null && !irreparableKeys.isEmpty() : irreparableKeys;
+
+        this.repairableKeys = repairableKeys;
+        this.irreparableKeys = irreparableKeys;
+    }
+
+    /**
+     * Inconsistent keys found but can not be fixed using the specified strategy.
+     */
+    public Collection<Object> irreparableKeys() {
+        return irreparableKeys;
+    }
+
+    /**
+     * Inconsistent keys found but can be fixed using the specified strategy.
+     */
+    public Collection<Object> repairableKeys() {
+        return repairableKeys;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/local/atomic/GridLocalAtomicCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/local/atomic/GridLocalAtomicCache.java
index 06490a8..b088a56 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/local/atomic/GridLocalAtomicCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/local/atomic/GridLocalAtomicCache.java
@@ -33,6 +33,7 @@
 import javax.cache.processor.EntryProcessorException;
 import javax.cache.processor.EntryProcessorResult;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.CacheEntryPredicate;
@@ -319,7 +320,7 @@
         boolean deserializeBinary,
         boolean needVer,
         boolean recovery,
-        boolean readRepair) throws IgniteCheckedException {
+        ReadRepairStrategy readRepairStrategy) throws IgniteCheckedException {
         A.notNull(keys, "keys");
 
         String taskName = ctx.kernalContext().job().currentTaskName();
@@ -341,7 +342,7 @@
         final String taskName,
         final boolean deserializeBinary,
         boolean recovery,
-        boolean readRepair,
+        ReadRepairStrategy readRepairStrategy,
         final boolean skipVals,
         final boolean needVer
     ) {
@@ -545,7 +546,7 @@
             taskName,
             deserializeBinary,
             opCtx != null && opCtx.recovery(),
-            false,
+            null,
             /*force primary*/false,
             expiry,
             skipVals,
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/MvccProcessorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/MvccProcessorImpl.java
index 8ff27b4..8e7bc72 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/MvccProcessorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/MvccProcessorImpl.java
@@ -975,8 +975,8 @@
         DataRegionConfiguration cfg = new DataRegionConfiguration();
 
         cfg.setName(TX_LOG_CACHE_NAME);
-        cfg.setInitialSize(dscfg.getSystemRegionInitialSize());
-        cfg.setMaxSize(dscfg.getSystemRegionMaxSize());
+        cfg.setInitialSize(dscfg.getSystemDataRegionConfiguration().getInitialSize());
+        cfg.setMaxSize(dscfg.getSystemDataRegionConfiguration().getMaxSize());
         cfg.setPersistenceEnabled(CU.isPersistenceEnabled(dscfg));
         cfg.setLazyMemoryAllocation(false);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogInnerIO.java
index 95c10ce..9ee69f4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogInnerIO.java
@@ -37,6 +37,8 @@
 
     /** {@inheritDoc} */
     @Override public void storeByOffset(long pageAddr, int off, TxKey row) {
+        assertPageType(pageAddr);
+
         TxRow row0 = (TxRow)row;
 
         setMajor(pageAddr, off, row0.major());
@@ -46,6 +48,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<TxKey> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         TxLogIO srcIo0 = (TxLogIO)srcIo;
 
         int srcOff = srcIo.offset(srcIdx);
@@ -80,6 +84,8 @@
 
     /** {@inheritDoc} */
     @Override public void setMajor(long pageAddr, int off, long major) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, off, major);
     }
 
@@ -90,6 +96,8 @@
 
     /** {@inheritDoc} */
     @Override public void setMinor(long pageAddr, int off, long minor) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, off + 8, minor);
     }
 
@@ -100,6 +108,8 @@
 
     /** {@inheritDoc} */
     @Override public void setState(long pageAddr, int off, byte state) {
+        assertPageType(pageAddr);
+
         PageUtils.putByte(pageAddr, off + 16, state);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogLeafIO.java
index e037fbe..4e1f608 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLogLeafIO.java
@@ -37,6 +37,8 @@
 
     /** {@inheritDoc} */
     @Override public void storeByOffset(long pageAddr, int off, TxKey row) {
+        assertPageType(pageAddr);
+
         TxRow row0 = (TxRow)row;
 
         setMajor(pageAddr, off, row0.major());
@@ -46,6 +48,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<TxKey> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         TxLogIO srcIo0 = (TxLogIO)srcIo;
 
         int srcOff = srcIo.offset(srcIdx);
@@ -80,6 +84,8 @@
 
     /** {@inheritDoc} */
     @Override public void setMajor(long pageAddr, int off, long major) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, off, major);
     }
 
@@ -90,6 +96,8 @@
 
     /** {@inheritDoc} */
     @Override public void setMinor(long pageAddr, int off, long minor) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, off + 8, minor);
     }
 
@@ -100,6 +108,8 @@
 
     /** {@inheritDoc} */
     @Override public void setState(long pageAddr, int off, byte state) {
+        assertPageType(pageAddr);
+
         PageUtils.putByte(pageAddr, off + 16, state);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
index c854bbf..0388588 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
@@ -499,8 +499,8 @@
         DataRegionConfiguration cfg = new DataRegionConfiguration();
 
         cfg.setName(METASTORE_DATA_REGION_NAME);
-        cfg.setInitialSize(storageCfg.getSystemRegionInitialSize());
-        cfg.setMaxSize(storageCfg.getSystemRegionMaxSize());
+        cfg.setInitialSize(storageCfg.getSystemDataRegionConfiguration().getInitialSize());
+        cfg.setMaxSize(storageCfg.getSystemDataRegionConfiguration().getMaxSize());
         cfg.setPersistenceEnabled(true);
         cfg.setLazyMemoryAllocation(false);
 
@@ -581,7 +581,8 @@
                 kernalCtx.longJvmPauseDetector(),
                 kernalCtx.failure(),
                 kernalCtx.cache(),
-                () -> cpFreqDeviation.getOrDefault(DEFAULT_CHECKPOINT_DEVIATION)
+                () -> cpFreqDeviation.getOrDefault(DEFAULT_CHECKPOINT_DEVIATION),
+                kernalCtx.pools().getSystemExecutorService()
             );
 
             final NodeFileLockHolder preLocked = kernalCtx.pdsFolderResolver()
@@ -2583,7 +2584,11 @@
                         try {
                             DataRecord dataRec = (DataRecord)rec;
 
-                            for (DataEntry dataEntry : dataRec.writeEntries()) {
+                            int entryCnt = dataRec.entryCount();
+
+                            for (int i = 0; i < entryCnt; i++) {
+                                DataEntry dataEntry = dataRec.get(i);
+
                                 if (entryPredicate.apply(dataEntry)) {
                                     checkpointReadLock();
 
@@ -2733,7 +2738,11 @@
                     case ENCRYPTED_DATA_RECORD_V3:
                         DataRecord dataRec = (DataRecord)rec;
 
-                        for (DataEntry dataEntry : dataRec.writeEntries()) {
+                        int entryCnt = dataRec.entryCount();
+
+                        for (int i = 0; i < entryCnt; i++) {
+                            DataEntry dataEntry = dataRec.get(i);
+
                             if (!restoreMeta && txManager.uncommitedTx(dataEntry))
                                 continue;
 
@@ -3248,7 +3257,8 @@
         checkpointReadLock();
 
         try {
-            CheckpointEntry lastCp = checkpointHistory().lastCheckpointMarkingAsInapplicable(grpId);
+            CheckpointEntry lastCp = checkpointManager.checkpointMarkerStorage().removeFromEarliestCheckpoints(grpId);
+
             long lastCpTs = lastCp != null ? lastCp.timestamp() : 0;
 
             if (lastCpTs != 0)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
index 6f3d917..0742afd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
@@ -372,8 +372,8 @@
         addDataRegion(
             memCfg,
             createSystemDataRegion(
-                memCfg.getSystemRegionInitialSize(),
-                memCfg.getSystemRegionMaxSize(),
+                memCfg.getSystemDataRegionConfiguration().getInitialSize(),
+                memCfg.getSystemDataRegionConfiguration().getMaxSize(),
                 persistenceEnabled
             ),
             persistenceEnabled
@@ -382,8 +382,8 @@
         addDataRegion(
             memCfg,
             createVolatileDataRegion(
-                memCfg.getSystemRegionInitialSize(),
-                memCfg.getSystemRegionMaxSize()
+                memCfg.getSystemDataRegionConfiguration().getInitialSize(),
+                memCfg.getSystemDataRegionConfiguration().getMaxSize()
             ),
             false
         );
@@ -581,8 +581,8 @@
         Set<String> regNames = new HashSet<>();
 
         checkSystemDataRegionSizeConfiguration(
-            memCfg.getSystemRegionInitialSize(),
-            memCfg.getSystemRegionMaxSize()
+            memCfg.getSystemDataRegionConfiguration().getInitialSize(),
+            memCfg.getSystemDataRegionConfiguration().getMaxSize()
         );
 
         Map<Class<? extends WarmUpConfiguration>, WarmUpStrategy> warmUpStrategies =
@@ -1276,7 +1276,11 @@
         boolean trackable,
         PageReadWriteManager pmPageMgr
     ) throws IgniteCheckedException {
-        PageMemory pageMem = createPageMemory(createOrReuseMemoryProvider(plcCfg), memCfg, plcCfg, memMetrics, trackable, pmPageMgr);
+        if (plcCfg.getMemoryAllocator() == null)
+            plcCfg.setMemoryAllocator(memCfg.getMemoryAllocator());
+
+        PageMemory pageMem = createPageMemory(createOrReuseMemoryProvider(plcCfg), memCfg, plcCfg, memMetrics,
+            trackable, pmPageMgr);
 
         return new DataRegion(pageMem, plcCfg, memMetrics, createPageEvictionTracker(plcCfg, pageMem));
     }
@@ -1315,7 +1319,7 @@
         File allocPath = buildAllocPath(plcCfg);
 
         return allocPath == null ?
-            new UnsafeMemoryProvider(log) :
+            new UnsafeMemoryProvider(log, plcCfg.getMemoryAllocator()) :
             new MappedFileMemoryProvider(
                 log,
                 allocPath);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IndexStorageImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IndexStorageImpl.java
index fb84d6a..99ef373 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IndexStorageImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IndexStorageImpl.java
@@ -543,6 +543,8 @@
 
         /** {@inheritDoc} */
         @Override public void storeByOffset(long buf, int off, IndexItem row) throws IgniteCheckedException {
+            assertPageType(buf);
+
             storeRow(buf, off, row);
         }
 
@@ -552,6 +554,8 @@
             BPlusIO<IndexItem> srcIo,
             long srcPageAddr,
             int srcIdx) throws IgniteCheckedException {
+            assertPageType(dstPageAddr);
+
             storeRow(dstPageAddr, offset(dstIdx), srcPageAddr, ((IndexIO)srcIo).getOffset(srcPageAddr, srcIdx));
         }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RecoveryDebug.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RecoveryDebug.java
index c36c9aa..c68d1cf 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RecoveryDebug.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RecoveryDebug.java
@@ -113,10 +113,15 @@
 
         append("Data record\n");
 
-        for (DataEntry dataEntry : rec.writeEntries())
+        int entryCnt = rec.entryCount();
+
+        for (int i = 0; i < entryCnt; i++) {
+            DataEntry dataEntry = rec.get(i);
+
             append("\t" + dataEntry.op() + " " + dataEntry.nearXidVersion() +
                 (unwrapKeyValue ? " " + dataEntry.key() + " " + dataEntry.value() : "") + "\n"
             );
+        }
 
         return this;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointEntry.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointEntry.java
index 7dbb507..0cbc168 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointEntry.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointEntry.java
@@ -35,6 +35,7 @@
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.IgniteSystemProperties.IGNITE_DISABLE_GRP_STATE_LAZY_STORE;
@@ -74,7 +75,11 @@
         this.cpTs = cpTs;
         this.cpMark = cpMark;
         this.cpId = cpId;
-        this.grpStateLazyStore = new SoftReference<>(new GroupStateLazyStore(cacheGrpStates));
+
+        if (cacheGrpStates == null)
+            this.grpStateLazyStore = new SoftReference<>(new GroupStateLazyStore(null));
+        else
+            this.grpStateLazyStore = new SoftReference<>(new GroupStateLazyStore(GroupStateLazyStore.remap(cacheGrpStates)));
     }
 
     /**
@@ -129,6 +134,24 @@
     }
 
     /**
+     * @return Checkpoint entry's group states.
+     */
+    @Nullable Map<Integer, GroupState> groupStates() {
+        GroupStateLazyStore store = grpStateLazyStore.get();
+
+        return store != null ? store.groupStates() : null;
+    }
+
+    /**
+     * Sets up {@link #grpStateLazyStore} from group states map.
+     *
+     * @param groupStates Checkpoint entry's group states map.
+     */
+    void fillStore(Map<Integer, GroupState> groupStates) {
+        grpStateLazyStore = new SoftReference<>(new GroupStateLazyStore(groupStates));
+    }
+
+    /**
      * @param wal Write ahead log manager.
      * @param grpId Cache group ID.
      * @param part Partition ID.
@@ -151,10 +174,10 @@
      */
     public static class GroupState {
         /** Partition ids. */
-        private int[] parts;
+        private final int[] parts;
 
         /** Partition counters which corresponds to partition ids. */
-        private long[] cnts;
+        private final long[] cnts;
 
         /** Next index to insert to parts and cnts. */
         private int idx;
@@ -168,6 +191,31 @@
         }
 
         /**
+         * @param parts Partitions' ids.
+         * @param cnts Partitions' counters.
+         * @param size Partitions count.
+         */
+        GroupState(int parts[], long[] cnts, int size) {
+            this.parts = parts;
+            this.cnts = cnts;
+            this.idx = size;
+        }
+
+        /**
+         * @return Partitions' ids.
+         */
+        int[] partitionIds() {
+            return parts;
+        }
+
+        /**
+         * @return Partitions' counters.
+         */
+        long[] partitionCounters() {
+            return cnts;
+        }
+
+        /**
          * @param partId Partition ID to add.
          * @param cntr Partition counter.
          */
@@ -244,7 +292,7 @@
             AtomicIntegerFieldUpdater.newUpdater(GroupStateLazyStore.class, "initGuard");
 
         /** Cache states. Initialized lazily. */
-        private volatile Map<Integer, GroupState> grpStates;
+        @Nullable private volatile Map<Integer, GroupState> grpStates;
 
         /** */
         private final CountDownLatch latch;
@@ -264,10 +312,12 @@
         }
 
         /**
-         * @param cacheGrpStates Cache group state.
+         * Constructor with group state map.
+         *
+         * @param stateMap Group state map.
          */
-        private GroupStateLazyStore(Map<Integer, CacheState> cacheGrpStates) {
-            if (cacheGrpStates != null) {
+        GroupStateLazyStore(@Nullable Map<Integer, GroupState> stateMap) {
+            if (stateMap != null) {
                 initGuard = 1;
 
                 latch = new CountDownLatch(0);
@@ -275,14 +325,14 @@
             else
                 latch = new CountDownLatch(1);
 
-            grpStates = remap(cacheGrpStates);
+            this.grpStates = stateMap;
         }
 
         /**
          * @param stateRec Cache group state.
          */
-        private Map<Integer, GroupState> remap(Map<Integer, CacheState> stateRec) {
-            if (stateRec == null)
+        private static Map<Integer, GroupState> remap(@NotNull Map<Integer, CacheState> stateRec) {
+            if (stateRec.isEmpty())
                 return Collections.emptyMap();
 
             Map<Integer, GroupState> grpStates = U.newHashMap(stateRec.size());
@@ -311,6 +361,13 @@
         }
 
         /**
+         * @return Partitions' states by group id.
+         */
+        @Nullable Map<Integer, GroupState> groupStates() {
+            return grpStates;
+        }
+
+        /**
          * @param grpId Group id.
          * @param part Partition id.
          * @return Partition counter.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointHistory.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointHistory.java
index aa77a57..9d319d8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointHistory.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointHistory.java
@@ -27,9 +27,11 @@
 import java.util.Map;
 import java.util.NavigableMap;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.IgniteSystemProperties;
@@ -37,6 +39,8 @@
 import org.apache.ignite.configuration.WALMode;
 import org.apache.ignite.internal.pagemem.wal.IgniteWriteAheadLogManager;
 import org.apache.ignite.internal.pagemem.wal.record.CacheState;
+import org.apache.ignite.internal.processors.cache.persistence.checkpoint.CheckpointEntry.GroupState;
+import org.apache.ignite.internal.processors.cache.persistence.checkpoint.EarliestCheckpointMapSnapshot.GroupStateSnapshot;
 import org.apache.ignite.internal.processors.cache.persistence.partstate.GroupPartitionId;
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
 import org.apache.ignite.internal.util.lang.IgniteThrowableBiPredicate;
@@ -113,14 +117,32 @@
 
     /**
      * @param checkpoints Checkpoints.
+     * @param snapshot Earliest checkpoint map snapshot.
      */
-    public void initialize(List<CheckpointEntry> checkpoints) {
+    public void initialize(
+        List<CheckpointEntry> checkpoints,
+        EarliestCheckpointMapSnapshot snapshot
+    ) {
         for (CheckpointEntry e : checkpoints)
             histMap.put(e.timestamp(), e);
 
         for (Long timestamp : checkpoints(false)) {
             try {
-                updateEarliestCpMap(entry(timestamp));
+                CheckpointEntry entry = entry(timestamp);
+
+                UUID checkpointId = entry.checkpointId();
+
+                Map<Integer, GroupState> groupStateMap = snapshot.groupState(checkpointId);
+
+                // Ignore checkpoint that was present at the time of the snapshot and whose group
+                // states map was not persisted (that means this checkpoint wasn't a part of earliestCp map)
+                if (snapshot.checkpointWasPresent(checkpointId) && groupStateMap == null)
+                    continue;
+
+                if (groupStateMap != null)
+                    entry.fillStore(groupStateMap);
+
+                updateEarliestCpMap(entry, groupStateMap);
             }
             catch (IgniteCheckedException e) {
                 U.warn(log, "Failed to process checkpoint, happened at " + U.format(timestamp) + '.', e);
@@ -203,10 +225,15 @@
      * Update map which stored the earliest checkpoint each partitions for groups.
      *
      * @param entry Checkpoint entry.
+     * @param groupStateMapFromSnapshot Group state map from the snapshot.
      */
-    private void updateEarliestCpMap(CheckpointEntry entry) {
+    private void updateEarliestCpMap(
+        CheckpointEntry entry,
+        @Nullable Map<Integer, GroupState> groupStateMapFromSnapshot
+    ) {
         try {
-            Map<Integer, CheckpointEntry.GroupState> states = entry.groupState(wal);
+            Map<Integer, GroupState> states = groupStateMapFromSnapshot != null ?
+                groupStateMapFromSnapshot : entry.groupState(wal);
 
             Iterator<Map.Entry<GroupPartitionId, CheckpointEntry>> iter = earliestCp.entrySet().iterator();
 
@@ -239,12 +266,13 @@
     }
 
     /**
-     * Prepare last checkpoint in history that will marked as inapplicable.
+     * Removes last checkpoint in history from the earliest checkpoints map by group id and returns the latest
+     * checkpoint in the history.
      *
      * @param grpId Group id.
-     * @return Checkpoint witch it'd be marked as inapplicable.
+     * @return Latest checkpoint in the history.
      */
-    public CheckpointEntry lastCheckpointMarkingAsInapplicable(Integer grpId) {
+    public CheckpointEntry removeFromEarliestCheckpoints(Integer grpId) {
         synchronized (earliestCp) {
             CheckpointEntry lastCp = lastCheckpoint();
 
@@ -282,10 +310,10 @@
      */
     private void addCpGroupStatesToEarliestCpMap(
         CheckpointEntry entry,
-        Map<Integer, CheckpointEntry.GroupState> cacheGrpStates
+        Map<Integer, GroupState> cacheGrpStates
     ) {
         for (Integer grpId : cacheGrpStates.keySet()) {
-            CheckpointEntry.GroupState grpState = cacheGrpStates.get(grpId);
+            GroupState grpState = cacheGrpStates.get(grpId);
 
             for (int pIdx = 0; pIdx < grpState.size(); pIdx++) {
                 int part = grpState.getPartitionByIndex(pIdx);
@@ -738,6 +766,47 @@
     }
 
     /**
+     * Creates a snapshot of {@link #earliestCp} map.
+     * Guarded by checkpoint read lock.
+     *
+     * @return Snapshot of a map.
+     */
+    public EarliestCheckpointMapSnapshot earliestCheckpointsMapSnapshot() {
+        Map<UUID, Map<Integer, GroupStateSnapshot>> data = new HashMap<>();
+
+        synchronized (earliestCp) {
+            Collection<CheckpointEntry> values = earliestCp.values();
+
+            for (CheckpointEntry cp : values) {
+                UUID checkpointId = cp.checkpointId();
+
+                if (data.containsKey(checkpointId))
+                    continue;
+
+                Map<Integer, GroupState> map = cp.groupStates();
+
+                if (map != null) {
+                    Map<Integer, GroupStateSnapshot> groupStates = new HashMap<>();
+
+                    map.forEach((k, v) ->
+                        groupStates.put(k, new GroupStateSnapshot(
+                            v.partitionIds(),
+                            v.partitionCounters(),
+                            v.size()
+                        ))
+                    );
+
+                    data.put(checkpointId, groupStates);
+                }
+            }
+        }
+
+        Set<UUID> ids = histMap.values().stream().map(CheckpointEntry::checkpointId).collect(Collectors.toSet());
+
+        return new EarliestCheckpointMapSnapshot(ids, data);
+    }
+
+    /**
      * Clear all cached data.
      */
     void clear() {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointManager.java
index 3d8c287..8a45435 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointManager.java
@@ -22,6 +22,7 @@
 import java.nio.ByteOrder;
 import java.util.Collection;
 import java.util.UUID;
+import java.util.concurrent.Executor;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import org.apache.ignite.IgniteCheckedException;
@@ -106,6 +107,7 @@
      * @param failureProcessor Failure processor.
      * @param cacheProcessor Cache processor.
      * @param cpFreqDeviation Distributed checkpoint frequency deviation.
+     * @param checkpointMapSnapshotExecutor Checkpoint map snapshot executor.
      * @throws IgniteCheckedException if fail.
      */
     public CheckpointManager(
@@ -126,7 +128,8 @@
         LongJVMPauseDetector longJvmPauseDetector,
         FailureProcessor failureProcessor,
         GridCacheProcessor cacheProcessor,
-        Supplier<Integer> cpFreqDeviation
+        Supplier<Integer> cpFreqDeviation,
+        Executor checkpointMapSnapshotExecutor
     ) throws IgniteCheckedException {
         CheckpointHistory cpHistory = new CheckpointHistory(
             persistenceCfg,
@@ -137,15 +140,18 @@
 
         FileIOFactory ioFactory = persistenceCfg.getFileIOFactory();
 
+        CheckpointReadWriteLock lock = new CheckpointReadWriteLock(logger);
+
         checkpointMarkersStorage = new CheckpointMarkersStorage(
+            igniteInstanceName,
             logger,
             cpHistory,
             ioFactory,
-            pageStoreManager.workDir().getAbsolutePath()
+            pageStoreManager.workDir().getAbsolutePath(),
+            lock,
+            checkpointMapSnapshotExecutor
         );
 
-        CheckpointReadWriteLock lock = new CheckpointReadWriteLock(logger);
-
         checkpointWorkflow = new CheckpointWorkflow(
             logger,
             wal,
@@ -262,6 +268,13 @@
     }
 
     /**
+     * @return Checkpoint storage.
+     */
+    public CheckpointMarkersStorage checkpointMarkerStorage() {
+        return checkpointMarkersStorage;
+    }
+
+    /**
      * @return Read checkpoint status.
      * @throws IgniteCheckedException If failed to read checkpoint status page.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointMarkersStorage.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointMarkersStorage.java
index 630b918..66b3824 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointMarkersStorage.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/CheckpointMarkersStorage.java
@@ -25,17 +25,23 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.internal.pagemem.wal.record.CacheState;
 import org.apache.ignite.internal.pagemem.wal.record.CheckpointRecord;
 import org.apache.ignite.internal.processors.cache.persistence.StorageException;
@@ -43,10 +49,13 @@
 import org.apache.ignite.internal.processors.cache.persistence.file.FileIOFactory;
 import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager;
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
+import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.marshaller.jdk.JdkMarshaller;
 import org.jetbrains.annotations.Nullable;
 
 import static java.nio.file.StandardOpenOption.READ;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD;
 import static org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager.TMP_FILE_MATCHER;
 
 /**
@@ -56,6 +65,21 @@
     /** Checkpoint file name pattern. */
     public static final Pattern CP_FILE_NAME_PATTERN = Pattern.compile("(\\d+)-(.*)-(START|END)\\.bin");
 
+    /** Earliest checkpoint map changes threshold. */
+    private final int earliestCpChangesThreshold = IgniteSystemProperties.getInteger(
+        IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD,
+        5
+    );
+
+    /** Earliest checkpoint map snapshot file name. */
+    public static final String EARLIEST_CP_SNAPSHOT_FILE = "cpMapSnapshot.bin";
+
+    /** Earliest checkpoint map snapshot temporary file name. */
+    private static final String EARLIEST_CP_SNAPSHOT_TMP_FILE = EARLIEST_CP_SNAPSHOT_FILE + ".tmp";
+
+    /** Checkpoint map snapshot executor. */
+    private final Executor checkpointMapSnapshotExecutor;
+
     /** Logger. */
     protected IgniteLogger log;
 
@@ -65,28 +89,44 @@
     /** File I/O factory for writing checkpoint markers. */
     private final FileIOFactory ioFactory;
 
+    /** Checkpoint read-write lock. */
+    private final CheckpointReadWriteLock lock;
+
     /** Checkpoint metadata directory ("cp"), contains files with checkpoint start and end */
     public final File cpDir;
 
     /** Temporary write buffer. */
     private final ByteBuffer tmpWriteBuf;
 
+    /** Counter representing quantity of checkpoints since last checkpoint history snapshot. */
+    private final AtomicInteger checkpointSnapshotCounter = new AtomicInteger(1);
+
+    /** Guards checkpoint snapshot operation, so that it couldn't run in parallel. */
+    private final AtomicBoolean checkpointSnapshotInProgress = new AtomicBoolean(false);
+
     /**
+     * @param igniteInstanceName Ignite instance name.
      * @param logger Ignite logger.
      * @param history Checkpoint history.
      * @param factory IO factory.
      * @param absoluteWorkDir Directory path to checkpoint markers folder.
+     * @param lock Checkpoint read-write lock.
+     * @param checkpointMapSnapshotExecutor Checkpoint map snapshot executor.
      * @throws IgniteCheckedException if fail.
      */
     CheckpointMarkersStorage(
+        String igniteInstanceName,
         Function<Class<?>, IgniteLogger> logger,
         CheckpointHistory history,
         FileIOFactory factory,
-        String absoluteWorkDir
+        String absoluteWorkDir,
+        CheckpointReadWriteLock lock,
+        Executor checkpointMapSnapshotExecutor
     ) throws IgniteCheckedException {
         this.log = logger.apply(getClass());
         cpHistory = history;
         ioFactory = factory;
+        this.lock = lock;
 
         cpDir = Paths.get(absoluteWorkDir, "cp").toFile();
 
@@ -98,6 +138,7 @@
 
         tmpWriteBuf.order(ByteOrder.nativeOrder());
 
+        this.checkpointMapSnapshotExecutor = checkpointMapSnapshotExecutor;
     }
 
     /**
@@ -137,7 +178,42 @@
      * Filling internal structures with data from disk.
      */
     public void initialize() throws IgniteCheckedException {
-        cpHistory.initialize(retrieveHistory());
+        File snapshotFile = new File(cpDir, EARLIEST_CP_SNAPSHOT_FILE);
+        File snapshotTmpFile = new File(cpDir, EARLIEST_CP_SNAPSHOT_TMP_FILE);
+
+        if (snapshotTmpFile.exists()) {
+            if (!IgniteUtils.delete(snapshotTmpFile)) {
+                throw new IgniteCheckedException(
+                    "Failed to remove invalid earliest checkpoint map snapshot temporary file: " + snapshotTmpFile +
+                        ". Remove it manually and restart the node."
+                );
+            }
+        }
+
+        EarliestCheckpointMapSnapshot snap = null;
+
+        if (snapshotFile.exists()) {
+            try {
+                byte[] bytes = Files.readAllBytes(snapshotFile.toPath());
+
+                snap = JdkMarshaller.DEFAULT.unmarshal(bytes, null);
+            }
+            catch (IOException e) {
+                log.error("Failed to unmarshal earliest checkpoint map snapshot", e);
+
+                if (!IgniteUtils.delete(snapshotFile)) {
+                    throw new IgniteCheckedException(
+                        "Failed to remove invalid earliest checkpoint map snapshot file: " + snapshotFile + ". " +
+                            "Remove it manually and restart the node."
+                    );
+                }
+            }
+        }
+
+        if (snap == null)
+            snap = new EarliestCheckpointMapSnapshot();
+
+        cpHistory.initialize(retrieveHistory(), snap);
     }
 
     /**
@@ -151,6 +227,8 @@
 
         for (CheckpointEntry cp : rmvFromHist)
             removeCheckpointFiles(cp);
+
+        onEarliestCheckpointMapChanged();
     }
 
     /**
@@ -164,6 +242,8 @@
 
         for (CheckpointEntry cp : rmvFromHist)
             removeCheckpointFiles(cp);
+
+        onEarliestCheckpointMapChanged();
     }
 
     /**
@@ -432,6 +512,8 @@
 
         writeCheckpointEntry(tmpWriteBuf, entry, type, skipSync);
 
+        onEarliestCheckpointMapChanged();
+
         return entry;
     }
 
@@ -495,4 +577,104 @@
     public CheckpointHistory history() {
         return cpHistory;
     }
+
+    /**
+     * See {@link CheckpointHistory#removeFromEarliestCheckpoints}.
+     *
+     * @param grpId Group id.
+     * @return Checkpoint entry.
+     */
+    public CheckpointEntry removeFromEarliestCheckpoints(Integer grpId) {
+        CheckpointEntry entry = cpHistory.removeFromEarliestCheckpoints(grpId);
+
+        onEarliestCheckpointMapChanged();
+
+        return entry;
+    }
+
+    /**
+     * Handles changes in the earliest checkpoint map in the {@link CheckpointHistory}. Creates a snapshot of that
+     * map if threshold for changes has been reached.
+     */
+    void onEarliestCheckpointMapChanged() {
+        boolean createSnapshot =
+            checkpointSnapshotCounter.getAndUpdate(old -> (old + 1) % earliestCpChangesThreshold) == 0;
+
+        if (createSnapshot) {
+            Runnable runnable = () -> {
+                if (!checkpointSnapshotInProgress.compareAndSet(false, true)) {
+                    return;
+                }
+
+                try {
+                    EarliestCheckpointMapSnapshot snapshot;
+
+                    lock.readLock();
+                    try {
+                        // Create the earliest checkpoint map snapshot
+                        snapshot = cpHistory.earliestCheckpointsMapSnapshot();
+                    }
+                    finally {
+                        lock.readUnlock();
+                    }
+
+                    File targetFile = new File(cpDir, EARLIEST_CP_SNAPSHOT_FILE);
+
+                    // For fail-safety we should first write the snapshot to a temporary file
+                    // and then atomically rename it
+                    File tmpFile = new File(cpDir, EARLIEST_CP_SNAPSHOT_TMP_FILE);
+
+                    if (tmpFile.exists() && !IgniteUtils.delete(tmpFile)) {
+                        log.error("Failed to delete temporary checkpoint snapshot file: " + tmpFile.getAbsolutePath());
+
+                        return;
+                    }
+
+                    final byte[] bytes;
+
+                    try {
+                        bytes = JdkMarshaller.DEFAULT.marshal(snapshot);
+                    }
+                    catch (IgniteCheckedException e) {
+                        log.error("Failed to marshal checkpoint snapshot: " + e.getMessage(), e);
+
+                        return;
+                    }
+
+                    try {
+                        Files.write(tmpFile.toPath(), bytes);
+                    }
+                    catch (IOException e) {
+                        log.error("Failed to write checkpoint snapshot temporary file: " + tmpFile, e);
+
+                        return;
+                    }
+
+                    try {
+                        // Atomically rename temporary file
+                        Files.move(
+                            tmpFile.toPath(),
+                            targetFile.toPath(),
+                            StandardCopyOption.ATOMIC_MOVE,
+                            StandardCopyOption.REPLACE_EXISTING
+                        );
+                    }
+                    catch (IOException e) {
+                        log.error("Failed to rename temporary checkpoint snapshot file: " + targetFile, e);
+                    }
+                }
+                finally {
+                    checkpointSnapshotInProgress.set(false);
+                }
+            };
+
+            try {
+                checkpointMapSnapshotExecutor.execute(runnable);
+            }
+            catch (RejectedExecutionException e) {
+                log.warning("Unable to capture a checkpoint map snapshot since node is shutting down: " +
+                    e.getMessage());
+            }
+        }
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/EarliestCheckpointMapSnapshot.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/EarliestCheckpointMapSnapshot.java
new file mode 100644
index 0000000..7c45e1a
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/checkpoint/EarliestCheckpointMapSnapshot.java
@@ -0,0 +1,177 @@
+/*
+ * 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.cache.persistence.checkpoint;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Earliest checkpoint map snapshot.
+ * Speeds up construction of the earliestCp map in the {@link CheckpointHistory}.
+ */
+public class EarliestCheckpointMapSnapshot extends IgniteDataTransferObject {
+    /** Serial version UUID. */
+    private static final long serialVersionUID = 0L;
+
+    /** Last snapshot's checkpoint timestamp. */
+    private Map</*Checkpoint id */ UUID, Map</* Group id */ Integer, GroupStateSnapshot>> data = new HashMap<>();
+
+    /** Ids of checkpoints present at the time of the snapshot capture. */
+    private Set<UUID> checkpointIds;
+
+    /** Constructor. */
+    public EarliestCheckpointMapSnapshot(
+        Set<UUID> checkpointIds,
+        Map<UUID, Map<Integer, GroupStateSnapshot>> earliestCp
+    ) {
+        this.checkpointIds = checkpointIds;
+        this.data = earliestCp;
+    }
+
+    /** Default constructor. */
+    public EarliestCheckpointMapSnapshot() {
+        checkpointIds = new HashSet<>();
+    }
+
+    /**
+     * Gets a group state by a checkpoint id.
+     *
+     * @param checkpointId Checkpoint id.
+     * @return Group state.
+     */
+    @Nullable public Map<Integer, CheckpointEntry.GroupState> groupState(UUID checkpointId) {
+        Map<Integer, GroupStateSnapshot> groupStateSnapshotMap = data.get(checkpointId);
+
+        Map<Integer, CheckpointEntry.GroupState> groupStateMap = null;
+
+        if (groupStateSnapshotMap != null) {
+            groupStateMap = new HashMap<>();
+
+            for (Map.Entry<Integer, GroupStateSnapshot> e : groupStateSnapshotMap.entrySet()) {
+                Integer k = e.getKey();
+                GroupStateSnapshot v = e.getValue();
+
+                groupStateMap.put(k, new CheckpointEntry.GroupState(
+                    v.partitionIds(),
+                    v.partitionCounters(),
+                    v.size()
+                ));
+            }
+
+        }
+        return groupStateMap;
+    }
+
+    /**
+     * Returns {@code true} if a checkpoint was present during the snapshot capture, {@code false} otherwise.
+     *
+     * @param checkpointId Checkpoint id.
+     * @return {@code true} if checkpoint was present, {@code false} otherwise.
+     */
+    public boolean checkpointWasPresent(UUID checkpointId) {
+        return checkpointIds.contains(checkpointId);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        U.writeMap(out, data);
+        U.writeCollection(out, checkpointIds);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(byte protoVer, ObjectInput in) throws IOException, ClassNotFoundException {
+        data = U.readMap(in);
+        checkpointIds = U.readSet(in);
+    }
+
+    /** {@link CheckpointEntry.GroupState} snapshot. */
+    static class GroupStateSnapshot extends IgniteDataTransferObject {
+        /** Serial version UUID. */
+        private static final long serialVersionUID = 0L;
+
+        /** Partition ids. */
+        private int[] parts;
+
+        /** Partition counters which corresponds to partition ids. */
+        private long[] cnts;
+
+        /** Partitions count. */
+        private int size;
+
+        /**
+         * @param parts Partitions' ids.
+         * @param cnts Partitions' counters.
+         * @param size Partitions count.
+         */
+        GroupStateSnapshot(int[] parts, long[] cnts, int size) {
+            this.parts = parts;
+            this.cnts = cnts;
+            this.size = size;
+        }
+
+        /**
+         * Constructor for serialization.
+         */
+        public GroupStateSnapshot() {
+        }
+
+        /**
+         * @return Partitions' ids.
+         */
+        int[] partitionIds() {
+            return parts;
+        }
+
+        /**
+         * @return Partitions' counters.
+         */
+        long[] partitionCounters() {
+            return cnts;
+        }
+
+        /**
+         * @return Partitions count.
+         */
+        public int size() {
+            return size;
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            U.writeIntArray(out, parts);
+            U.writeLongArray(out, cnts);
+            out.writeInt(size);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte protoVer, ObjectInput in) throws IOException, ClassNotFoundException {
+            parts = U.readIntArray(in);
+            cnts = U.readLongArray(in);
+            size = in.readInt();
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/defragmentation/LinkMap.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/defragmentation/LinkMap.java
index 237a22a..e44e8c3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/defragmentation/LinkMap.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/defragmentation/LinkMap.java
@@ -222,6 +222,8 @@
 
         /** {@inheritDoc} */
         @Override public void storeByOffset(long pageAddr, int off, LinkMapping row) {
+            assertPageType(pageAddr);
+
             PageUtils.putLong(pageAddr, off, row.getOldLink());
             PageUtils.putLong(pageAddr, off + Long.BYTES, row.getNewLink());
         }
@@ -252,6 +254,8 @@
 
         /** {@inheritDoc} */
         @Override public void storeByOffset(long pageAddr, int off, LinkMapping row) {
+            assertPageType(pageAddr);
+
             PageUtils.putLong(pageAddr, off, row.getOldLink());
             PageUtils.putLong(pageAddr, off + Long.BYTES, row.getNewLink());
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
index c9c0e56..6c5507a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
@@ -78,6 +78,7 @@
      */
     private void setCount(long pageAddr, int cnt) {
         assert cnt >= 0 && cnt <= Short.MAX_VALUE : cnt;
+        assertPageType(pageAddr);
 
         PageUtils.putShort(pageAddr, CNT_OFF, (short)cnt);
     }
@@ -95,6 +96,8 @@
      * @param metaPageId Next meta page ID.
      */
     public void setNextMetaPageId(long pageAddr, long metaPageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, NEXT_META_PAGE_OFF, metaPageId);
     }
 
@@ -115,6 +118,7 @@
      */
     public int addTails(int pageSize, long pageAddr, int bucket, PagesList.Stripe[] tails, int tailsOff) {
         assert bucket >= 0 && bucket <= Short.MAX_VALUE : bucket;
+        assertPageType(pageAddr);
 
         int cnt = getCount(pageAddr);
         int cap = getCapacity(pageSize, pageAddr);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
index 6c754a0..42126dd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
@@ -88,6 +88,8 @@
      * @param nextId Next page ID.
      */
     public void setNextId(long pageAddr, long nextId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, NEXT_PAGE_ID_OFF, nextId);
     }
 
@@ -104,6 +106,8 @@
      * @param prevId Previous  page ID.
      */
     public void setPreviousId(long pageAddr, long prevId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, PREV_PAGE_ID_OFF, prevId);
     }
 
@@ -125,6 +129,7 @@
      */
     private void setCount(long pageAddr, int cnt) {
         assert cnt >= 0 && cnt <= Short.MAX_VALUE : cnt;
+        assertPageType(pageAddr);
 
         PageUtils.putShort(pageAddr, CNT_OFF, (short)cnt);
     }
@@ -162,6 +167,8 @@
      * @param pageId Item value to write.
      */
     private void setAt(long pageAddr, int idx, long pageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset(idx), pageId);
     }
 
@@ -174,6 +181,8 @@
      * @return Total number of items in this page.
      */
     public int addPage(long pageAddr, long pageId, int pageSize) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         if (cnt == getCapacity(pageSize))
@@ -192,6 +201,8 @@
      * @return Removed page ID.
      */
     public long takeAnyPage(long pageAddr) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         if (cnt == 0)
@@ -211,6 +222,7 @@
      */
     public boolean removePage(long pageAddr, long dataPageId) {
         assert dataPageId != 0;
+        assertPageType(pageAddr);
 
         int cnt = getCount(pageAddr);
 
@@ -238,6 +250,8 @@
 
     /** {@inheritDoc} */
     @Override public void compactPage(ByteBuffer page, ByteBuffer out, int pageSize) {
+        assertPageType(page);
+
         copyPage(page, out, pageSize);
 
         long pageAddr = GridUnsafe.bufferAddress(out);
@@ -251,6 +265,7 @@
         assert compactPage.isDirect();
         assert compactPage.position() == 0;
         assert compactPage.limit() <= pageSize;
+        assertPageType(compactPage);
 
         compactPage.limit(pageSize); // Just add garbage to the end.
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageInnerIO.java
index 3153d7e..a6451f8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageInnerIO.java
@@ -39,6 +39,8 @@
         int off,
         MetastorageRow row
     ) {
+        assertPageType(pageAddr);
+
         // All update operations should use new IO version.
         setVersion(pageAddr, 2);
 
@@ -47,6 +49,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<MetastorageRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         // All update operations should use new IO version.
         setVersion(dstPageAddr, 2);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageLeafIO.java
index 777c103..0b3a8c1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetastorageLeafIO.java
@@ -39,6 +39,8 @@
         int off,
         MetastorageRow row
     ) {
+        assertPageType(pageAddr);
+
         setVersion(pageAddr, 2);
 
         MetastoragePageIOUtils.storeByOffset(this, pageAddr, off, row);
@@ -46,6 +48,8 @@
 
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<MetastorageRow> srcIo, long srcPageAddr, int srcIdx) {
+        assertPageType(dstPageAddr);
+
         setVersion(dstPageAddr, 2);
 
         MetastoragePageIOUtils.store(this, dstPageAddr, dstIdx, srcIo, srcPageAddr, srcIdx);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
index ba26086..9da686e 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
@@ -929,6 +929,12 @@
 
                     tryToRestorePage(fullId, buf);
 
+                    // Mark the page as dirty because it has been restored.
+                    setDirty(fullId, lockedPageAbsPtr, true, false);
+
+                    // And save the page snapshot in the WAL.
+                    beforeReleaseWrite(fullId, pageAddr, true);
+
                     statHolder.trackPhysicalAndLogicalRead(pageAddr);
 
                     dataRegionMetrics.onPageRead();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/AbstractSnapshotVerificationTask.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/AbstractSnapshotVerificationTask.java
index 1f2a48a..addf246 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/AbstractSnapshotVerificationTask.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/AbstractSnapshotVerificationTask.java
@@ -23,6 +23,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.compute.ComputeJob;
@@ -63,21 +64,11 @@
         Set<SnapshotMetadata> allMetas = new HashSet<>();
         clusterMetas.values().forEach(allMetas::addAll);
 
-        Set<String> missed = null;
-
-        for (SnapshotMetadata meta : allMetas) {
-            if (missed == null)
-                missed = new HashSet<>(meta.baselineNodes());
-
-            missed.remove(meta.consistentId());
-
-            if (missed.isEmpty())
-                break;
+        try {
+            checkMissedMetadata(allMetas);
         }
-
-        if (!missed.isEmpty()) {
-            throw new IgniteSnapshotVerifyException(F.asMap(ignite.localNode(),
-                new IgniteException("Some metadata is missing from the snapshot: " + missed)));
+        catch (IgniteCheckedException e) {
+            throw new IgniteSnapshotVerifyException(F.asMap(ignite.localNode(), new IgniteException(e.getMessage())));
         }
 
         metas.putAll(clusterMetas);
@@ -107,6 +98,29 @@
     }
 
     /**
+     * Ensures that all parts of the snapshot are available according to the metadata.
+     *
+     * @param clusterMetas List of snapshot metadata found in the cluster.
+     * @throws IgniteCheckedException If some metadata is missing.
+     */
+    public static void checkMissedMetadata(Collection<SnapshotMetadata> clusterMetas) throws IgniteCheckedException {
+        Set<String> missed = null;
+
+        for (SnapshotMetadata meta : clusterMetas) {
+            if (missed == null)
+                missed = new HashSet<>(meta.baselineNodes());
+
+            missed.remove(meta.consistentId());
+
+            if (missed.isEmpty())
+                break;
+        }
+
+        if (!missed.isEmpty())
+            throw new IgniteCheckedException("Some metadata is missing from the snapshot: " + missed);
+    }
+
+    /**
      * @param name Snapshot name.
      * @param constId Snapshot metadata file name.
      * @param groups Cache groups to be restored from the snapshot. May be empty if all cache groups are being restored.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
index 7207d57..12e2945 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
@@ -59,7 +59,6 @@
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiConsumer;
@@ -100,6 +99,7 @@
 import org.apache.ignite.internal.managers.communication.TransmissionMeta;
 import org.apache.ignite.internal.managers.communication.TransmissionPolicy;
 import org.apache.ignite.internal.managers.eventstorage.DiscoveryEventListener;
+import org.apache.ignite.internal.managers.systemview.walker.SnapshotViewWalker;
 import org.apache.ignite.internal.pagemem.store.PageStore;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.CacheGroupDescriptor;
@@ -160,8 +160,7 @@
 import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.marshaller.MarshallerUtils;
 import org.apache.ignite.resources.IgniteInstanceResource;
-import org.apache.ignite.thread.IgniteThreadPoolExecutor;
-import org.apache.ignite.thread.OomExceptionHandler;
+import org.apache.ignite.spi.systemview.view.SnapshotView;
 import org.jetbrains.annotations.Nullable;
 
 import static java.nio.file.StandardOpenOption.READ;
@@ -210,6 +209,8 @@
 import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.END_SNAPSHOT;
 import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.START_SNAPSHOT;
 import static org.apache.ignite.plugin.security.SecurityPermission.ADMIN_SNAPSHOT;
+import static org.apache.ignite.spi.systemview.view.SnapshotView.SNAPSHOT_SYS_VIEW;
+import static org.apache.ignite.spi.systemview.view.SnapshotView.SNAPSHOT_SYS_VIEW_DESC;
 
 /**
  * Internal implementation of snapshot operations over persistence caches.
@@ -255,7 +256,7 @@
     public static final String SNAPSHOT_METAFILE_EXT = ".smf";
 
     /** Prefix for snapshot threads. */
-    private static final String SNAPSHOT_RUNNER_THREAD_PREFIX = "snapshot-runner";
+    public static final String SNAPSHOT_RUNNER_THREAD_PREFIX = "snapshot-runner";
 
     /** Snapshot operation finish log message. */
     private static final String SNAPSHOT_FINISHED_MSG = "Cluster-wide snapshot operation finished successfully: ";
@@ -263,9 +264,6 @@
     /** Snapshot operation fail log message. */
     private static final String SNAPSHOT_FAILED_MSG = "Cluster-wide snapshot operation failed: ";
 
-    /** Total number of thread to perform local snapshot. */
-    private static final int SNAPSHOT_THREAD_POOL_SIZE = 4;
-
     /** Default snapshot topic to receive snapshots from remote node. */
     private static final Object DFLT_INITIAL_SNAPSHOT_TOPIC = GridTopic.TOPIC_SNAPSHOT.topic("rmt_snp");
 
@@ -343,9 +341,6 @@
     /** File store manager to create page store for restore. */
     private volatile FilePageStoreManager storeMgr;
 
-    /** Snapshot thread pool to perform local partition snapshots. */
-    private ExecutorService snpRunner;
-
     /** System discovery message listener. */
     private DiscoveryEventListener discoLsnr;
 
@@ -420,15 +415,6 @@
         if (!CU.isPersistenceEnabled(ctx.config()))
             return;
 
-        snpRunner = new IgniteThreadPoolExecutor(SNAPSHOT_RUNNER_THREAD_PREFIX,
-            cctx.igniteInstanceName(),
-            SNAPSHOT_THREAD_POOL_SIZE,
-            SNAPSHOT_THREAD_POOL_SIZE,
-            IgniteConfiguration.DFLT_THREAD_KEEP_ALIVE_TIME,
-            new LinkedBlockingQueue<>(),
-            SYSTEM_POOL,
-            new OomExceptionHandler(ctx));
-
         assert cctx.pageStore() instanceof FilePageStoreManager;
 
         storeMgr = (FilePageStoreManager)cctx.pageStore();
@@ -441,7 +427,7 @@
         U.ensureDirectory(locSnpDir, "snapshot work directory", log);
         U.ensureDirectory(tmpWorkDir, "temp directory for snapshot creation", log);
 
-        handlers.initialize(ctx, snpRunner);
+        handlers.initialize(ctx, ctx.pools().getSnapshotExecutorService());
 
         MetricRegistry mreg = cctx.kernalContext().metric().registry(SNAPSHOT_METRICS);
 
@@ -460,6 +446,8 @@
             "The list of names of all snapshots currently saved on the local node with respect to " +
                 "the configured via IgniteConfiguration snapshot working path.");
 
+        restoreCacheGrpProc.registerMetrics();
+
         cctx.exchange().registerExchangeAwareComponent(this);
 
         ctx.internalSubscriptionProcessor().registerMetastorageListener(this);
@@ -501,6 +489,13 @@
 
         cctx.gridIO().addMessageListener(DFLT_INITIAL_SNAPSHOT_TOPIC, snpRmtMgr);
         cctx.kernalContext().io().addTransmissionHandler(DFLT_INITIAL_SNAPSHOT_TOPIC, snpRmtMgr);
+
+        ctx.systemView().registerView(
+            SNAPSHOT_SYS_VIEW,
+            SNAPSHOT_SYS_VIEW_DESC,
+            new SnapshotViewWalker(),
+            () -> F.flatCollections(F.transform(localSnapshotNames(), this::readSnapshotMetadatas)),
+            this::snapshotViewSupplier);
     }
 
     /** {@inheritDoc} */
@@ -526,9 +521,6 @@
                 }
             }
 
-            if (snpRunner != null)
-                snpRunner.shutdownNow();
-
             cctx.kernalContext().io().removeMessageListener(DFLT_INITIAL_SNAPSHOT_TOPIC);
             cctx.kernalContext().io().removeTransmissionHandler(DFLT_INITIAL_SNAPSHOT_TOPIC);
 
@@ -1262,9 +1254,14 @@
         A.notNullOrEmpty(snpName, "Snapshot name cannot be null or empty.");
         A.ensure(U.alphanumericUnderscore(snpName), "Snapshot name must satisfy the following name pattern: a-zA-Z0-9_");
 
+        File snpDir = snapshotLocalDir(snpName);
+
+        if (!(snpDir.exists() && snpDir.isDirectory()))
+            return Collections.emptyList();
+
         List<File> smfs = new ArrayList<>();
 
-        try (DirectoryStream<Path> ds = Files.newDirectoryStream(snapshotLocalDir(snpName).toPath())) {
+        try (DirectoryStream<Path> ds = Files.newDirectoryStream(snpDir.toPath())) {
             for (Path d : ds) {
                 if (Files.isRegularFile(d) && d.getFileName().toString().toLowerCase().endsWith(SNAPSHOT_METAFILE_EXT))
                     smfs.add(d.toFile());
@@ -1275,7 +1272,7 @@
         }
 
         if (smfs.isEmpty())
-            throw new IgniteException("Snapshot metadata files not found: " + snpName);
+            return Collections.emptyList();
 
         Map<String, SnapshotMetadata> metasMap = new HashMap<>();
         SnapshotMetadata prev = null;
@@ -1761,7 +1758,7 @@
      */
     RemoteSnapshotSender remoteSnapshotSenderFactory(String rqId, UUID nodeId) {
         return new RemoteSnapshotSender(log,
-            snpRunner,
+            cctx.kernalContext().pools().getSnapshotExecutorService(),
             databaseRelativePath(pdsSettings.folderName()),
             cctx.gridIO().openTransmissionSender(nodeId, DFLT_INITIAL_SNAPSHOT_TOPIC),
             rqId);
@@ -1802,22 +1799,20 @@
      * @return The executor used to run snapshot tasks.
      */
     ExecutorService snapshotExecutorService() {
-        assert snpRunner != null;
-
-        return snpRunner;
+        return cctx.kernalContext().pools().getSnapshotExecutorService();
     }
 
     /**
      * @param ioFactory Factory to create IO interface over a page stores.
      */
-    void ioFactory(FileIOFactory ioFactory) {
+    public void ioFactory(FileIOFactory ioFactory) {
         this.ioFactory = ioFactory;
     }
 
     /**
      * @return Factory to create IO interface over a page stores.
      */
-    FileIOFactory ioFactory() {
+    public FileIOFactory ioFactory() {
         return ioFactory;
     }
 
@@ -1900,6 +1895,17 @@
         return new IgniteFutureImpl<>(cctx.kernalContext().task().execute(taskCls, snpName));
     }
 
+    /**
+     * @param meta Snapshot metadata.
+     * @return Snapshot view.
+     */
+    private SnapshotView snapshotViewSupplier(SnapshotMetadata meta) {
+        Collection<String> cacheGrps = F.viewReadOnly(snapshotCacheDirectories(meta.snapshotName(), meta.folderName()),
+            FilePageStoreManager::cacheGroupName);
+
+        return new SnapshotView(meta.snapshotName(), meta.consistentId(), F.concat(meta.baselineNodes(), ","), F.concat(cacheGrps, ","));
+    }
+
     /** @return Snapshot handlers. */
     protected SnapshotHandlers handlers() {
         return handlers;
@@ -2907,7 +2913,7 @@
          * @param snpName Snapshot name.
          */
         public LocalSnapshotSender(String snpName) {
-            super(IgniteSnapshotManager.this.log, snpRunner);
+            super(IgniteSnapshotManager.this.log, cctx.kernalContext().pools().getSnapshotExecutorService());
 
             this.snpName = snpName;
             snpLocDir = snapshotLocalDir(snpName);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotMXBeanImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotMXBeanImpl.java
index 9dc1361..60abf07 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotMXBeanImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotMXBeanImpl.java
@@ -17,7 +17,11 @@
 
 package org.apache.ignite.internal.processors.cache.persistence.snapshot;
 
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.mxbean.SnapshotMXBean;
 
@@ -47,4 +51,20 @@
     @Override public void cancelSnapshot(String snpName) {
         mgr.cancelSnapshot(snpName).get();
     }
+
+    /** {@inheritDoc} */
+    @Override public void restoreSnapshot(String name, String grpNames) {
+        Set<String> grpNamesSet = F.isEmpty(grpNames) ? null :
+            Arrays.stream(grpNames.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
+
+        IgniteFuture<Void> fut = mgr.restoreSnapshot(name, grpNamesSet);
+
+        if (fut.isDone())
+            fut.get();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void cancelSnapshotRestore(String name) {
+        mgr.cancelSnapshotRestore(name).get();
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotRestoreProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotRestoreProcess.java
index c796299..b54e372 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotRestoreProcess.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotRestoreProcess.java
@@ -33,18 +33,17 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiPredicate;
 import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
-import java.util.function.IntFunction;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteIllegalStateException;
@@ -66,6 +65,7 @@
 import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager;
 import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.ClusterSnapshotFuture;
 import org.apache.ignite.internal.processors.cluster.DiscoveryDataClusterState;
+import org.apache.ignite.internal.processors.metric.MetricRegistry;
 import org.apache.ignite.internal.util.distributed.DistributedProcess;
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
@@ -101,6 +101,9 @@
     /** Temporary cache directory prefix. */
     public static final String TMP_CACHE_DIR_PREFIX = "_tmp_snp_restore_";
 
+    /** Snapshot restore metrics prefix. */
+    public static final String SNAPSHOT_RESTORE_METRICS = "snapshot-restore";
+
     /** Reject operation message. */
     private static final String OP_REJECT_MSG = "Cache group restore operation was rejected. ";
 
@@ -131,9 +134,12 @@
     /** Future to be completed when the cache restore process is complete (this future will be returned to the user). */
     private volatile ClusterSnapshotFuture fut;
 
-    /** Snapshot restore operation context. */
+    /** Current snapshot restore operation context (will be {@code null} when the operation is not running). */
     private volatile SnapshotRestoreContext opCtx;
 
+    /** Last snapshot restore operation context (saves the metrics of the last operation). */
+    private volatile SnapshotRestoreContext lastOpCtx = new SnapshotRestoreContext();
+
     /**
      * @param ctx Kernal context.
      */
@@ -174,6 +180,30 @@
     }
 
     /**
+     * Register local metrics.
+     */
+    protected void registerMetrics() {
+        assert !ctx.clientNode();
+
+        MetricRegistry mreg = ctx.metric().registry(SNAPSHOT_RESTORE_METRICS);
+
+        mreg.register("startTime", () -> lastOpCtx.startTime,
+            "The system time of the start of the cluster snapshot restore operation on this node.");
+        mreg.register("endTime", () -> lastOpCtx.endTime,
+            "The system time when the restore operation of a cluster snapshot on this node ended.");
+        mreg.register("snapshotName", () -> lastOpCtx.snpName, String.class,
+            "The snapshot name of the last running cluster snapshot restore operation on this node.");
+        mreg.register("requestId", () -> Optional.ofNullable(lastOpCtx.reqId).map(UUID::toString).orElse(""),
+            String.class, "The request ID of the last running cluster snapshot restore operation on this node.");
+        mreg.register("error", () -> Optional.ofNullable(lastOpCtx.err.get()).map(Throwable::toString).orElse(""),
+            String.class, "Error message of the last running cluster snapshot restore operation on this node.");
+        mreg.register("totalPartitions", () -> lastOpCtx.totalParts,
+            "The total number of partitions to be restored on this node.");
+        mreg.register("processedPartitions", () -> lastOpCtx.processedParts.get(),
+            "The number of processed partitions on this node.");
+    }
+
+    /**
      * Start cache group restore operation.
      *
      * @param snpName Snapshot name.
@@ -239,7 +269,7 @@
         });
 
         String msg = "Cluster-wide snapshot restore operation started [reqId=" + fut0.rqId + ", snpName=" + snpName +
-            (cacheGrpNames == null ? "" : ", grps=" + cacheGrpNames) + ']';
+            (cacheGrpNames == null ? "" : ", caches=" + cacheGrpNames) + ']';
 
         if (log.isInfoEnabled())
             log.info(msg);
@@ -390,15 +420,6 @@
      * Finish local cache group restore process.
      *
      * @param reqId Request ID.
-     */
-    private void finishProcess(UUID reqId) {
-        finishProcess(reqId, null);
-    }
-
-    /**
-     * Finish local cache group restore process.
-     *
-     * @param reqId Request ID.
      * @param err Error, if any.
      */
     private void finishProcess(UUID reqId, @Nullable Throwable err) {
@@ -409,9 +430,12 @@
 
         SnapshotRestoreContext opCtx0 = opCtx;
 
-        if (opCtx0 != null && reqId.equals(opCtx0.reqId))
+        if (opCtx0 != null && reqId.equals(opCtx0.reqId)) {
             opCtx = null;
 
+            opCtx0.endTime = U.currentTimeMillis();
+        }
+
         synchronized (this) {
             ClusterSnapshotFuture fut0 = fut;
 
@@ -557,10 +581,12 @@
                     ", caches=" + req.groups() + ']');
             }
 
-            SnapshotRestoreContext opCtx0 = prepareContext(req);
+            List<SnapshotMetadata> locMetas = snpMgr.readSnapshotMetadatas(req.snapshotName());
+
+            SnapshotRestoreContext opCtx0 = prepareContext(req, locMetas);
 
             synchronized (this) {
-                opCtx = opCtx0;
+                lastOpCtx = opCtx = opCtx0;
 
                 ClusterSnapshotFuture fut0 = fut;
 
@@ -579,8 +605,7 @@
             if (ctx.isStopping())
                 throw new NodeStoppingException("The node is stopping: " + ctx.localNodeId());
 
-            return new GridFinishedFuture<>(new SnapshotRestoreOperationResponse(opCtx0.cfgs.values(),
-                opCtx0.metasPerNode.get(ctx.localNodeId())));
+            return new GridFinishedFuture<>(new SnapshotRestoreOperationResponse(opCtx0.cfgs.values(), locMetas));
         }
         catch (IgniteIllegalStateException | IgniteCheckedException | RejectedExecutionException e) {
             log.error("Unable to restore cache group(s) from the snapshot " +
@@ -600,18 +625,20 @@
 
     /**
      * @param req Request to prepare cache group restore from the snapshot.
+     * @param metas Local snapshot metadatas.
      * @return Snapshot restore operation context.
      * @throws IgniteCheckedException If failed.
      */
-    private SnapshotRestoreContext prepareContext(SnapshotOperationRequest req) throws IgniteCheckedException {
+    private SnapshotRestoreContext prepareContext(
+        SnapshotOperationRequest req,
+        Collection<SnapshotMetadata> metas
+    ) throws IgniteCheckedException {
         if (opCtx != null) {
             throw new IgniteCheckedException(OP_REJECT_MSG +
                 "The previous snapshot restore operation was not completed.");
         }
         GridCacheSharedContext<?, ?> cctx = ctx.cache().context();
 
-        List<SnapshotMetadata> metas = cctx.snapshotMgr().readSnapshotMetadatas(req.snapshotName());
-
         // Collection of baseline nodes that must survive and additional discovery data required for the affinity calculation.
         DiscoCache discoCache = ctx.discovery().discoCache();
 
@@ -620,8 +647,8 @@
 
         DiscoCache discoCache0 = discoCache.copy(discoCache.version(), null);
 
-        if (F.first(metas) == null)
-            return new SnapshotRestoreContext(req, discoCache0, Collections.emptyMap(), cctx.localNodeId(), Collections.emptyList());
+        if (F.isEmpty(metas))
+            return new SnapshotRestoreContext(req, discoCache0, Collections.emptyMap());
 
         if (F.first(metas).pageSize() != cctx.database().pageSize()) {
             throw new IgniteCheckedException("Incompatible memory page size " +
@@ -677,7 +704,7 @@
         Map<Integer, StoredCacheData> cfgsById =
             cfgsByName.values().stream().collect(Collectors.toMap(v -> CU.cacheId(v.config().getName()), v -> v));
 
-        return new SnapshotRestoreContext(req, discoCache0, cfgsById, cctx.localNodeId(), metas);
+        return new SnapshotRestoreContext(req, discoCache0, cfgsById);
     }
 
     /**
@@ -722,8 +749,7 @@
                 }
             }
 
-            opCtx0.metasPerNode.computeIfAbsent(e.getKey(), id -> new ArrayList<>())
-                .addAll(e.getValue().metas);
+            opCtx0.metasPerNode.put(e.getKey(), new ArrayList<>(e.getValue().metas));
         }
 
         opCtx0.cfgs = globalCfgs;
@@ -797,6 +823,11 @@
             if (ctx.isStopping())
                 throw new NodeStoppingException("Node is stopping: " + ctx.localNodeId());
 
+            Set<SnapshotMetadata> allMetas =
+                opCtx0.metasPerNode.values().stream().flatMap(List::stream).collect(Collectors.toSet());
+
+            AbstractSnapshotVerificationTask.checkMissedMetadata(allMetas);
+
             IgniteSnapshotManager snpMgr = ctx.cache().context().snapshotMgr();
 
             synchronized (this) {
@@ -804,9 +835,10 @@
             }
 
             if (log.isInfoEnabled()) {
-                log.info("Starting snapshot preload operation to restore cache groups" +
-                    "[snapshot=" + opCtx0.snpName +
-                    ", caches=" + F.transform(opCtx0.dirs, File::getName) + ']');
+                log.info("Starting snapshot preload operation to restore cache groups " +
+                    "[reqId=" + reqId +
+                    ", snapshot=" + opCtx0.snpName +
+                    ", caches=" + F.transform(opCtx0.dirs, FilePageStoreManager::cacheGroupName) + ']');
             }
 
             CompletableFuture<Void> metaFut = ctx.localNodeId().equals(opCtx0.opNodeId) ?
@@ -827,17 +859,19 @@
                         }
                     }, snpMgr.snapshotExecutorService()) : CompletableFuture.completedFuture(null);
 
+            Map<String, GridAffinityAssignmentCache> affCache = new HashMap<>();
+
             for (StoredCacheData data : opCtx0.cfgs.values()) {
-                opCtx0.affCache.computeIfAbsent(CU.cacheOrGroupName(data.config()),
+                affCache.computeIfAbsent(CU.cacheOrGroupName(data.config()),
                     grp -> calculateAffinity(ctx, data.config(), opCtx0.discoCache));
             }
 
-            // First preload everything from the local node.
-            List<SnapshotMetadata> locMetas = opCtx0.metasPerNode.get(ctx.localNodeId());
-
+            Map<Integer, Set<PartitionRestoreFuture>> allParts = new HashMap<>();
             Map<Integer, Set<PartitionRestoreFuture>> rmtLoadParts = new HashMap<>();
             ClusterNode locNode = ctx.cache().context().localNode();
+            List<SnapshotMetadata> locMetas = opCtx0.metasPerNode.get(locNode.id());
 
+            // First preload everything from the local node.
             for (File dir : opCtx0.dirs) {
                 String cacheOrGrpName = cacheGroupName(dir);
                 int grpId = CU.cacheId(cacheOrGrpName);
@@ -847,10 +881,26 @@
 
                 Set<PartitionRestoreFuture> leftParts;
 
-                opCtx0.locProgress.put(grpId,
-                    nodeAffinityPartitions(opCtx0.affCache.get(cacheOrGrpName), locNode, PartitionRestoreFuture::new));
+                // Partitions contained in the snapshot.
+                Set<Integer> availParts = new HashSet<>();
 
-                rmtLoadParts.put(grpId, leftParts = new HashSet<>(opCtx0.locProgress.get(grpId)));
+                for (SnapshotMetadata meta : allMetas) {
+                    Set<Integer> parts = meta.partitions().get(grpId);
+
+                    if (parts != null)
+                        availParts.addAll(parts);
+                }
+
+                List<List<ClusterNode>> assignment = affCache.get(cacheOrGrpName).idealAssignment().assignment();
+                Set<PartitionRestoreFuture> partFuts = availParts
+                    .stream()
+                    .filter(p -> p != INDEX_PARTITION && assignment.get(p).contains(locNode))
+                    .map(p -> new PartitionRestoreFuture(p, opCtx0.processedParts))
+                    .collect(Collectors.toSet());
+
+                allParts.put(grpId, partFuts);
+
+                rmtLoadParts.put(grpId, leftParts = new HashSet<>(partFuts));
 
                 if (leftParts.isEmpty())
                     continue;
@@ -863,7 +913,7 @@
                     if (leftParts.isEmpty())
                         break;
 
-                    File snpCacheDir = new File(ctx.cache().context().snapshotMgr().snapshotLocalDir(opCtx.snpName),
+                    File snpCacheDir = new File(ctx.cache().context().snapshotMgr().snapshotLocalDir(opCtx0.snpName),
                         Paths.get(databaseRelativePath(meta.folderName()), dir.getName()).toString());
 
                     leftParts.removeIf(partFut -> {
@@ -873,7 +923,7 @@
 
                         if (doCopy) {
                             copyLocalAsync(ctx.cache().context().snapshotMgr(),
-                                opCtx,
+                                opCtx0,
                                 snpCacheDir,
                                 tmpCacheDir,
                                 partFut);
@@ -887,7 +937,8 @@
 
                         if (log.isInfoEnabled()) {
                             log.info("The snapshot was taken on the same cluster topology. The index will be copied to " +
-                                "restoring cache group if necessary [snpName=" + opCtx0.snpName + ", dir=" + dir.getName() + ']');
+                                "restoring cache group if necessary [reqId=" + reqId + ", snapshot=" + opCtx0.snpName +
+                                ", dir=" + dir.getName() + ']');
                         }
 
                         File idxFile = new File(snpCacheDir, FilePageStoreManager.getPartitionFileName(INDEX_PARTITION));
@@ -895,11 +946,11 @@
                         if (idxFile.exists()) {
                             PartitionRestoreFuture idxFut;
 
-                            opCtx0.locProgress.computeIfAbsent(grpId, g -> new HashSet<>())
-                                .add(idxFut = new PartitionRestoreFuture(INDEX_PARTITION));
+                            allParts.computeIfAbsent(grpId, g -> new HashSet<>())
+                                .add(idxFut = new PartitionRestoreFuture(INDEX_PARTITION, opCtx0.processedParts));
 
                             copyLocalAsync(ctx.cache().context().snapshotMgr(),
-                                opCtx,
+                                opCtx0,
                                 snpCacheDir,
                                 tmpCacheDir,
                                 idxFut);
@@ -920,7 +971,7 @@
                     .filter(e -> !e.getKey().equals(ctx.localNodeId()))
                     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
                 (grpId, partId) -> rmtLoadParts.get(grpId) != null &&
-                    rmtLoadParts.get(grpId).remove(new PartitionRestoreFuture(partId)));
+                    rmtLoadParts.get(grpId).remove(new PartitionRestoreFuture(partId, opCtx0.processedParts)));
 
             Map<Integer, File> grpToDir = opCtx0.dirs.stream()
                 .collect(Collectors.toMap(d -> CU.cacheId(FilePageStoreManager.cacheGroupName(d)),
@@ -928,8 +979,9 @@
 
             try {
                 if (log.isInfoEnabled() && !snpAff.isEmpty()) {
-                    log.info("Trying to request partitions from remote nodes" +
-                        "[snapshot=" + opCtx0.snpName +
+                    log.info("Trying to request partitions from remote nodes " +
+                        "[reqId=" + reqId +
+                        ", snapshot=" + opCtx0.snpName +
                         ", map=" + snpAff.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
                         e -> partitionsMapToCompactString(e.getValue()))) + ']');
                 }
@@ -948,7 +1000,7 @@
                                     int grpId = CU.cacheId(cacheGroupName(snpFile.getParentFile()));
                                     int partId = partId(snpFile.getName());
 
-                                    PartitionRestoreFuture partFut = F.find(opCtx0.locProgress.get(grpId),
+                                    PartitionRestoreFuture partFut = F.find(allParts.get(grpId),
                                         null,
                                         new IgnitePredicate<PartitionRestoreFuture>() {
                                             @Override public boolean apply(PartitionRestoreFuture f) {
@@ -963,10 +1015,7 @@
                                     Path partFile = Paths.get(tmpCacheDir.getAbsolutePath(), snpFile.getName());
 
                                     try {
-                                        IgniteSnapshotManager.copy(snpMgr.ioFactory(),
-                                            snpFile,
-                                            partFile.toFile(),
-                                            snpFile.length());
+                                        Files.move(snpFile.toPath(), partFile);
 
                                         partFut.complete(partFile);
                                     }
@@ -987,12 +1036,14 @@
                 completeListExceptionally(rmtAwaitParts, e);
             }
 
-            List<PartitionRestoreFuture> allParts = opCtx0.locProgress.values().stream().flatMap(Collection::stream)
+            List<PartitionRestoreFuture> allPartFuts = allParts.values().stream().flatMap(Collection::stream)
                 .collect(Collectors.toList());
 
-            int size = allParts.size();
+            int size = allPartFuts.size();
 
-            CompletableFuture.allOf(allParts.toArray(new CompletableFuture[size]))
+            opCtx0.totalParts = size;
+
+            CompletableFuture.allOf(allPartFuts.toArray(new CompletableFuture[size]))
                 .runAfterBothAsync(metaFut, () -> {
                     try {
                         if (opCtx0.stopChecker.getAsBoolean())
@@ -1045,8 +1096,6 @@
         opCtx0.errHnd.accept(failure);
 
         if (failure != null) {
-            opCtx0.locStopCachesCompleteFut.onDone((Void)null);
-
             if (U.isLocalNodeCoordinator(ctx.discovery()))
                 rollbackRestoreProc.start(reqId, reqId);
 
@@ -1104,7 +1153,7 @@
             orElse(checkNodeLeft(opCtx0.nodes(), res.keySet()));
 
         if (failure == null) {
-            finishProcess(reqId);
+            finishProcess(reqId, null);
 
             return;
         }
@@ -1302,22 +1351,6 @@
     }
 
     /**
-     * @param affCache Affinity cache.
-     * @param node Cluster node to get assigned partitions.
-     * @return The set of partitions assigned to the given node.
-     */
-    private static <T> Set<T> nodeAffinityPartitions(
-        GridAffinityAssignmentCache affCache,
-        ClusterNode node,
-        IntFunction<T> factory
-    ) {
-        return IntStream.range(0, affCache.partitions())
-            .filter(p -> affCache.idealAssignment().assignment().get(p).contains(node))
-            .mapToObj(factory)
-            .collect(Collectors.toSet());
-    }
-
-    /**
      * @param col Collection of sets to complete.
      * @param ex Exception to set.
      */
@@ -1371,44 +1404,46 @@
         /** Stop condition checker. */
         private final BooleanSupplier stopChecker = () -> err.get() != null;
 
-        /** Progress of processing cache group partitions on the local node.*/
-        private final Map<Integer, Set<PartitionRestoreFuture>> locProgress = new HashMap<>();
-
-        /**
-         * The stop future responsible for stopping cache groups during the rollback phase. Will be completed when the rollback
-         * process executes and all the cache group stop actions completes (the processCacheStopRequestOnExchangeDone finishes
-         * successfully and all the data deleted from disk).
-         */
-        private final GridFutureAdapter<Void> locStopCachesCompleteFut = new GridFutureAdapter<>();
-
-        /** Calculated affinity assignment cache per each cache group. */
-        private final Map<String, GridAffinityAssignmentCache> affCache = new ConcurrentHashMap<>();
-
         /** Cache ID to configuration mapping. */
-        private volatile Map<Integer, StoredCacheData> cfgs;
+        private volatile Map<Integer, StoredCacheData> cfgs = Collections.emptyMap();
 
         /** Graceful shutdown future. */
         private volatile IgniteFuture<?> stopFut;
 
+        /** Operation start time. */
+        private final long startTime;
+
+        /** Number of processed (copied) partitions. */
+        private final AtomicInteger processedParts = new AtomicInteger(0);
+
+        /** Total number of partitions to be restored. */
+        private volatile int totalParts = -1;
+
+        /** Operation end time. */
+        private volatile long endTime;
+
+        /** Creates an empty context. */
+        protected SnapshotRestoreContext() {
+            reqId = null;
+            snpName = "";
+            startTime = 0;
+            opNodeId = null;
+            discoCache = null;
+        }
+
         /**
          * @param req Request to prepare cache group restore from the snapshot.
+         * @param discoCache Baseline discovery cache for node IDs that must be alive to complete the operation.
          * @param cfgs Cache ID to configuration mapping.
          */
-        protected SnapshotRestoreContext(
-            SnapshotOperationRequest req,
-            DiscoCache discoCache,
-            Map<Integer, StoredCacheData> cfgs,
-            UUID locNodeId,
-            List<SnapshotMetadata> locMetas
-        ) {
+        protected SnapshotRestoreContext(SnapshotOperationRequest req, DiscoCache discoCache, Map<Integer, StoredCacheData> cfgs) {
             reqId = req.requestId();
             snpName = req.snapshotName();
             opNodeId = req.operationalNodeId();
+            startTime = U.currentTimeMillis();
+
             this.discoCache = discoCache;
-
             this.cfgs = cfgs;
-
-            metasPerNode.computeIfAbsent(locNodeId, id -> new ArrayList<>()).addAll(locMetas);
         }
 
         /**
@@ -1448,11 +1483,23 @@
         /** Partition id. */
         private final int partId;
 
+        /** Counter of the total number of processed partitions. */
+        private final AtomicInteger cntr;
+
         /**
          * @param partId Partition id.
+         * @param cntr Counter of the total number of processed partitions.
          */
-        private PartitionRestoreFuture(int partId) {
+        private PartitionRestoreFuture(int partId, AtomicInteger cntr) {
             this.partId = partId;
+            this.cntr = cntr;
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean complete(Path path) {
+            cntr.incrementAndGet();
+
+            return super.complete(path);
         }
 
         /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
index 49dc924..7fe8bca 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
@@ -94,7 +94,69 @@
 import static org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIoResolver.DEFAULT_PAGE_IO_RESOLVER;
 
 /**
- * Abstract B+Tree.
+ * <h3>Abstract B+Tree.</h3>
+ *
+ * B+Tree is a block-based tree structure. Each block is represented with the page ({@link PageIO}) and contains a
+ * single tree node. There are two types of pages/nodes: {@link BPlusInnerIO} and {@link BPlusLeafIO}.
+ * <p/>
+ * Every page in the tree contains a list of <i>items</i>. Item is just a fixed-size binary payload.
+ * Inner nodes and leaves may have different item sizes. There's a limit on how many items each page can hold.
+ * It is defined by a {@link BPlusIO#getMaxCount(long, int)} method of the corresponding IO. There should be no empty
+ * pages in trees, so it's expected to have from {@code 1} to {@code max} items in every page.
+ * <p/>
+ * Items might have different meaning depending on the type of the page. In case of leaves, every item must describe a
+ * key and a value. In case of inner nodes, items describe only keys if {@link #canGetRowFromInner} is {@code false},
+ * or a key and a value otherwise. Items in every page are sorted according to the order dscribed by
+ * {@link #compare(BPlusIO, long, int, Object)} method. Specifics of the data stored in items are defined in the
+ * implementation and generally don't matter.
+ * <p/>
+ * All pages in the tree are divided into levels. Leaves are always at the level {@code 0}. Levels of inner pages are
+ * thus positive. Each level represents a singly linked list - each page has a link to the <i>forward</i> page at the
+ * same level. It can be retrieved by calling {@link BPlusIO#getForward(long)}. This link must be a zero if there's no
+ * forward page. Forward links on level {@code 0} allows iterating trees keys and values effectively without traversing
+ * any inner nodes ({@code AbstractForwardCursor}). Forward links in inner nodes have different purpose, more on that
+ * later.
+ * <p/>
+ * Leaves have no links other than forward links. But inner nodes also have links to their children nodes. Every inner
+ * node can be viewed like the following structure:
+ * <pre><code>
+ *       item(0)     item(1)        ...          item(N-1)
+ * link(0)     link(1)     link(2)  ...  link(N-1)       link(N)
+ * </code></pre>
+ * There are {@code N} items and {@code N+1} links. Each link points to page of a lower level. For example, pages on
+ * level {@code 2} always point to pages of level {@code 1}. For an item {@code i} left subtree is defined by
+ * {@code link(i)} and right subtree is defined by {@code link(i+1)} ({@link BPlusInnerIO#getLeft(long, int)} and
+ * {@link BPlusInnerIO#getRight(long, int)}). All items in the left subtree are less or equal to the original item
+ * (basic property for the trees).
+ * <p/>
+ * There's one more important property of these links: {@code forward(left(i)) == right(i)}. It is called a
+ * <i>triangle invariant</i>. More information on B+Tree structure can easily be found online. Following documentation
+ * concentrates more on specifics of this particular B+Tree implementation.
+ * <p/>
+ * <h3>General operations.</h3>
+ * This implementation allows for concurrent reads and update. Given that each page locks individually, there are
+ * general rules to avoid deadlocks.
+ * <ul>
+ *     <li>
+ *         Pages within a level always locked from left to right.
+ *     </li>
+ *     <li>
+ *         If there's already a lock on the page of level X then no locks should be acquired on levels less than X.
+ *         In other words, locks are aquired from the bottom to the top. The only exception to this rule is the
+ *         allocation of a new page on a lower level that no one sees yet.
+ *         </li>
+ * </ul>
+ * All basic operations fit into a similar pattern. First, the search is performed ({@link Get}). It goes recursively
+ * from the root to the leaf (if it's needed). On each level several outcomes are possible.
+ * <ul>
+ *     <li>Exact value is found and operation can be completed.</li>
+ *     <li>Insertion point is found and recursive procedure continues on the lower level.</li>
+ *     <li>Insertion point is not found due to concurrent modifications, but retry in the same node is possible.</li>
+ *     <li>Insertion point is not found due to concurrent modifications, but retry in the same node is impossible.</li>
+ * </ul>
+ * All these options, and more, are described in the class {@link Result}. Please refer to its usages for specifics of
+ * each operation. Once the path and the leaf for put/remove is found, the operation is then performed from the bottom
+ * to the top. Specifics are described in corresponding classes ({@link Put}, {@link Remove}).
  */
 @SuppressWarnings({"ConstantValueVariableUse"})
 public abstract class BPlusTree<L, T extends L> extends DataStructure implements IgniteTree<L, T> {
@@ -614,8 +676,7 @@
     /** */
     private class LockTailExact extends GetPageHandler<Update> {
         /** {@inheritDoc} */
-        @Override protected Result run0(long pageId, long page, long pageAddr, BPlusIO<L> io, Update u, int lvl)
-            throws IgniteCheckedException {
+        @Override protected Result run0(long pageId, long page, long pageAddr, BPlusIO<L> io, Update u, int lvl) {
             // Check the triangle invariant.
             if (io.getForward(pageAddr) != u.fwdId)
                 return RETRY;
@@ -1038,13 +1099,14 @@
 
     /**
      * @param upper Upper bound.
+     * @param upIncl {@code true} if upper bound is inclusive.
      * @param c Filter closure.
      * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
      * @return Cursor.
      * @throws IgniteCheckedException If failed.
      */
-    private GridCursor<T> findLowerUnbounded(L upper, TreeRowClosure<L, T> c, Object x) throws IgniteCheckedException {
-        ForwardCursor cursor = new ForwardCursor(null, upper, c, x);
+    private GridCursor<T> findLowerUnbounded(L upper, boolean upIncl, TreeRowClosure<L, T> c, Object x) throws IgniteCheckedException {
+        ForwardCursor cursor = new ForwardCursor(upper, upIncl, c, x);
 
         long firstPageId;
 
@@ -1107,13 +1169,34 @@
      * @throws IgniteCheckedException If failed.
      */
     public GridCursor<T> find(L lower, L upper, TreeRowClosure<L, T> c, Object x) throws IgniteCheckedException {
+        return find(lower, upper, true, true, c, x);
+    }
+
+    /**
+     * @param lower Lower bound or {@code null} if unbounded.
+     * @param upper Upper bound or {@code null} if unbounded.
+     * @param lowIncl {@code true} if lower bound is inclusive.
+     * @param upIncl {@code true} if upper bound is inclusive.
+     * @param c Filter closure.
+     * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
+     * @return Cursor.
+     * @throws IgniteCheckedException If failed.
+     */
+    public GridCursor<T> find(
+        L lower,
+        L upper,
+        boolean lowIncl,
+        boolean upIncl,
+        TreeRowClosure<L, T> c,
+        Object x
+    ) throws IgniteCheckedException {
         checkDestroyed();
 
-        ForwardCursor cursor = new ForwardCursor(lower, upper, c, x);
+        ForwardCursor cursor = new ForwardCursor(lower, upper, lowIncl, upIncl, c, x);
 
         try {
             if (lower == null)
-                return findLowerUnbounded(upper, c, x);
+                return findLowerUnbounded(upper, upIncl, c, x);
 
             cursor.find();
 
@@ -4450,7 +4533,7 @@
     /**
      * Remove operation.
      */
-    private final class Remove extends Update implements ReuseBag {
+    public final class Remove extends Update implements ReuseBag {
         /** */
         Bool needReplaceInner = FALSE;
 
@@ -5496,22 +5579,30 @@
         /** */
         L lowerBound;
 
-        /** */
-        private int lowerShift = -1; // Initially it is -1 to handle multiple equal rows.
+        /** Handle multiple equal rows on lower side. */
+        private int lowerShift;
 
         /** */
         final L upperBound;
 
+        /** Handle multiple equal rows on upper side. */
+        private final int upperShift;
+
         /** Cached value for retrieving diagnosting info in case of failure. */
         public GetCursor getCursor;
 
         /**
          * @param lowerBound Lower bound.
          * @param upperBound Upper bound.
+         * @param lowIncl {@code true} if lower bound is inclusive.
+         * @param upIncl {@code true} if upper bound is inclusive.
          */
-        AbstractForwardCursor(L lowerBound, L upperBound) {
+        AbstractForwardCursor(L lowerBound, L upperBound, boolean lowIncl, boolean upIncl) {
             this.lowerBound = lowerBound;
             this.upperBound = upperBound;
+
+            lowerShift = lowIncl ? -1 : 1;
+            upperShift = upIncl ? 1 : -1;
         }
 
         /**
@@ -5602,8 +5693,8 @@
             // Compare with the last row on the page.
             int cmp = compare(0, io, pageAddr, cnt - 1, upperBound);
 
-            if (cmp > 0) {
-                int idx = findInsertionPoint(0, io, pageAddr, low, cnt, upperBound, 1);
+            if (cmp > 0 || (cmp == 0 && upperShift == -1)) {
+                int idx = findInsertionPoint(0, io, pageAddr, low, cnt, upperBound, upperShift);
 
                 assert idx < 0;
 
@@ -5734,12 +5825,12 @@
         private L lastRow;
 
         /**
-         * @param lowerBound Lower bound.
-         * @param upperBound Upper bound.
+         * @param lowerBound Lower bound inclusive.
+         * @param upperBound Upper bound inclusive.
          * @param p Row predicate.
          */
         ClosureCursor(L lowerBound, L upperBound, TreeRowClosure<L, T> p) {
-            super(lowerBound, upperBound);
+            super(lowerBound, upperBound, true, true);
 
             assert lowerBound != null;
             assert upperBound != null;
@@ -5835,13 +5926,27 @@
         private final TreeRowClosure<L, T> c;
 
         /**
-         * @param lowerBound Lower bound.
+         * Lower unbound cursor.
+         *
          * @param upperBound Upper bound.
+         * @param upIncl {@code true} if upper bound is inclusive.
          * @param c Filter closure.
          * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
          */
-        ForwardCursor(L lowerBound, L upperBound, TreeRowClosure<L, T> c, Object x) {
-            super(lowerBound, upperBound);
+        ForwardCursor(L upperBound, boolean upIncl, TreeRowClosure<L, T> c, Object x) {
+            this(null, upperBound, true, upIncl, c, x);
+        }
+
+        /**
+         * @param lowerBound Lower bound.
+         * @param upperBound Upper bound.
+         * @param lowIncl {@code true} if lower bound is inclusive.
+         * @param upIncl {@code true} if upper bound is inclusive.
+         * @param c Filter closure.
+         * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
+         */
+        ForwardCursor(L lowerBound, L upperBound, boolean lowIncl, boolean upIncl, TreeRowClosure<L, T> c, Object x) {
+            super(lowerBound, upperBound, lowIncl, upIncl);
 
             this.c = c;
             this.x = x;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
index 82e565f..9063fc9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
@@ -238,6 +238,8 @@
      * @param freeListPageId Free list page ID.
      */
     public void setFreeListPageId(long pageAddr, long freeListPageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, FREE_LIST_PAGE_ID_OFF, freeListPageId);
     }
 
@@ -295,6 +297,7 @@
      */
     private void setFirstEntryOffset(long pageAddr, int dataOff, int pageSize) {
         assert dataOff >= ITEMS_OFF + ITEM_SIZE && dataOff <= pageSize : dataOff;
+        assertPageType(pageAddr);
 
         PageUtils.putShort(pageAddr, FIRST_ENTRY_OFF, (short)dataOff);
     }
@@ -314,6 +317,7 @@
      */
     private void setRealFreeSpace(long pageAddr, int freeSpace, int pageSize) {
         assert freeSpace == actualFreeSpace(pageAddr, pageSize) : freeSpace + " != " + actualFreeSpace(pageAddr, pageSize);
+        assertPageType(pageAddr);
 
         PageUtils.putShort(pageAddr, FREE_SPACE_OFF, (short)freeSpace);
     }
@@ -364,6 +368,7 @@
      */
     private void setDirectCount(long pageAddr, int cnt) {
         assert checkCount(cnt) : cnt;
+        assertPageType(pageAddr);
 
         PageUtils.putByte(pageAddr, DIRECT_CNT_OFF, (byte)cnt);
     }
@@ -387,16 +392,18 @@
     /**
      * @param pageAddr Page address.
      * @param c Closure.
-     * @param <T> Closure return type.
+     * @param <U> Closure return type.
      * @return Collection of closure results for all items in page.
      * @throws IgniteCheckedException In case of error in closure body.
      */
-    public <T> List<T> forAllItems(long pageAddr, CC<T> c) throws IgniteCheckedException {
+    public <U> List<U> forAllItems(long pageAddr, CC<U> c) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         long pageId = getPageId(pageAddr);
 
         int cnt = getDirectCount(pageAddr);
 
-        List<T> res = new ArrayList<>(cnt);
+        List<U> res = new ArrayList<>(cnt);
 
         for (int i = 0; i < cnt; i++) {
             long link = PageIdUtils.link(pageId, i);
@@ -666,6 +673,8 @@
      * @param item Item.
      */
     private void setItem(long pageAddr, int idx, short item) {
+        assertPageType(pageAddr);
+
         PageUtils.putShort(pageAddr, itemOffset(idx), item);
     }
 
@@ -797,6 +806,7 @@
         final int rowSize) throws IgniteCheckedException {
         assert checkIndex(itemId) : itemId;
         assert row != null ^ payload != null;
+        assertPageType(pageAddr);
 
         final int dataOff = getDataOffset(pageAddr, itemId, pageSize);
 
@@ -820,6 +830,7 @@
      */
     public long removeRow(long pageAddr, int itemId, int pageSize) throws IgniteCheckedException {
         assert checkIndex(itemId) : itemId;
+        assertPageType(pageAddr);
 
         final int dataOff = getDataOffset(pageAddr, itemId, pageSize);
         final long nextLink = isFragmented(pageAddr, dataOff) ? getNextFragmentLink(pageAddr, dataOff) : 0;
@@ -935,6 +946,7 @@
         final int pageSize
     ) throws IgniteCheckedException {
         assert rowSize <= getFreeSpace(pageAddr) : "can't call addRow if not enough space for the whole row";
+        assertPageType(pageAddr);
 
         int fullEntrySize = getPageEntrySize(rowSize, SHOW_PAYLOAD_LEN | SHOW_ITEM);
 
@@ -965,6 +977,7 @@
         int pageSize
     ) throws IgniteCheckedException {
         assert payload.length <= getFreeSpace(pageAddr) : "can't call addRow if not enough space for the whole row";
+        assertPageType(pageAddr);
 
         int fullEntrySize = getPageEntrySize(payload.length, SHOW_PAYLOAD_LEN | SHOW_ITEM);
 
@@ -995,6 +1008,8 @@
         int dataOff,
         int pageSize
     ) {
+        assertPageType(pageAddr);
+
         if (!isEnoughSpace(entryFullSize, dataOff, directCnt, indirectCnt)) {
             dataOff = compactDataEntries(pageAddr, directCnt, pageSize);
 
@@ -1078,6 +1093,8 @@
         int rowSize,
         int pageSize
     ) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         return addRowFragment(pageMem, pageId, pageAddr, written, rowSize, row.link(), row, null, pageSize);
     }
 
@@ -1098,6 +1115,8 @@
         long lastLink,
         int pageSize
     ) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         addRowFragment(null, pageId, pageAddr, 0, 0, lastLink, null, payload, pageSize);
     }
 
@@ -1243,6 +1262,8 @@
 
     /** {@inheritDoc} */
     @Override public void compactPage(ByteBuffer page, ByteBuffer out, int pageSize) {
+        assertPageType(page);
+
         // TODO May we compactDataEntries in-place and then copy compacted data to out?
         copyPage(page, out, pageSize);
 
@@ -1275,6 +1296,7 @@
         assert page.isDirect();
         assert page.position() == 0;
         assert page.limit() <= pageSize;
+        assertPageType(page);
 
         long pageAddr = bufferAddress(page);
 
@@ -1449,6 +1471,8 @@
         int dataOff,
         byte[] payload
     ) {
+        assertPageType(pageAddr);
+
         PageUtils.putShort(pageAddr, dataOff, (short)payload.length);
         dataOff += 2;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
index 1f72342..eb7cedb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
@@ -28,6 +28,40 @@
 
 /**
  * Abstract IO routines for B+Tree pages.
+ * <p/>
+ * Every B+Tree page has a similar structure:
+ * <pre><code>
+ *     | HEADER | count | forwardId | removeId | items... |
+ * </code></pre>
+ * {@code HEADER} is a common structure that's present in every page. Please refer to {@link PageIO} and
+ * {@link PageIO#COMMON_HEADER_END} specifically for more details.
+ * <p/>
+ * {@code count} ({@link #getCount(long)}) is an unsigned short value that represents a number of {@code items} in the
+ * page. What the {@code item} is exactly is defined by specific implementations. Item size is defined by a
+ * {@link #itemSize} constant. Two implementations of the IO handle items list differently:
+ * <ul>
+ *     <li>
+ *         {@link BPlusLeafIO} uses an array to store all items, with no gaps inbetween:
+ *         <pre><code>
+ * | item0 | item1 | ... | itemN-2 | itemN-1 |
+ *         </code></pre>
+ *     </li>
+ *     <li>
+ *         {@link BPlusInnerIO} interlaces items arrays with links array. It looks like this:
+ *         <pre><code>
+ * | link0 | item0 | link1 | item1 | ... | linkN-1 | itemN-1 | linkN |
+ *         </code></pre>
+ *         This layout affects the way offset is calculated and the total amount of items that can be put into a single
+ *         page.
+ *     </li>
+ * </ul>
+ * {@code forwardId} ({@link #getForward(long)}) is a link to the forward page, please refer to {@link BPlusTree} for
+ * the explanation.
+ * <p/>
+ * {@code removeId} ({@link #getRemoveId(long)}) is a special value that's used to check tree invariants durning
+ * deletions. Please refer to {@link BPlusTree} for better explanation.
+ *
+ * @see BPlusTree
  */
 public abstract class BPlusIO<L> extends PageIO implements CompactablePageIO {
     /** */
@@ -97,6 +131,8 @@
      * @param pageId Forward page ID.
      */
     public final void setForward(long pageAddr, long pageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, FORWARD_OFF, pageId);
 
         assert getForward(pageAddr) == pageId;
@@ -115,6 +151,8 @@
      * @param rmvId Remove ID.
      */
     public final void setRemoveId(long pageAddr, long rmvId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, REMOVE_ID_OFF, rmvId);
 
         assert getRemoveId(pageAddr) == rmvId;
@@ -137,6 +175,8 @@
      * @param cnt Count.
      */
     public final void setCount(long pageAddr, int cnt) {
+        assertPageType(pageAddr);
+
         assert cnt >= 0 : cnt;
 
         PageUtils.putShort(pageAddr, CNT_OFF, (short)cnt);
@@ -180,6 +220,8 @@
      */
     public final byte[] store(long pageAddr, int idx, L row, byte[] rowBytes, boolean needRowBytes)
         throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         int off = offset(idx);
 
         if (rowBytes == null) {
@@ -263,6 +305,8 @@
      */
     public byte[] insert(long pageAddr, int idx, L row, byte[] rowBytes, long rightId, boolean needRowBytes)
         throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         // Move right all the greater elements to make a free slot for a new row link.
@@ -291,6 +335,8 @@
         int pageSize,
         PageMetrics metrics
     ) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         initNewPage(fwdPageAddr, fwdId, pageSize, metrics);
 
         cnt -= mid;
@@ -311,6 +357,8 @@
      * @param fwdId New forward page ID.
      */
     public void splitExistingPage(long pageAddr, int mid, long fwdId) {
+        assertPageType(pageAddr);
+
         setCount(pageAddr, mid);
         setForward(pageAddr, fwdId);
     }
@@ -322,6 +370,8 @@
      * @throws IgniteCheckedException If failed.
      */
     public void remove(long pageAddr, int idx, int cnt) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         cnt--;
 
         copyItems(pageAddr, pageAddr, idx + 1, idx, cnt - idx, false);
@@ -348,6 +398,8 @@
         boolean emptyBranch,
         int pageSize
     ) throws IgniteCheckedException {
+        assertPageType(leftPageAddr);
+
         int prntCnt = prntIo.getCount(prntPageAddr);
         int leftCnt = getCount(leftPageAddr);
         int rightCnt = getCount(rightPageAddr);
@@ -428,6 +480,8 @@
 
     /** {@inheritDoc} */
     @Override public void compactPage(ByteBuffer page, ByteBuffer out, int pageSize) {
+        assertPageType(page);
+
         copyPage(page, out, pageSize);
 
         long pageAddr = GridUnsafe.bufferAddress(out);
@@ -438,6 +492,8 @@
 
     /** {@inheritDoc} */
     @Override public void restorePage(ByteBuffer compactPage, int pageSize) {
+        assertPageType(compactPage);
+
         assert compactPage.isDirect();
         assert compactPage.position() == 0;
         assert compactPage.limit() <= pageSize;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusInnerIO.java
index 5e5bc1c..45b990f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusInnerIO.java
@@ -68,6 +68,8 @@
      * @param pageId Page ID.
      */
     public final void setLeft(long pageAddr, int idx, long pageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset0(idx, SHIFT_LEFT), pageId);
 
         assert pageId == getLeft(pageAddr, idx);
@@ -88,6 +90,8 @@
      * @param pageId Page ID.
      */
     private void setRight(long pageAddr, int idx, long pageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset0(idx, SHIFT_RIGHT), pageId);
 
         assert pageId == getRight(pageAddr, idx);
@@ -96,6 +100,8 @@
     /** {@inheritDoc} */
     @Override public final void copyItems(long srcPageAddr, long dstPageAddr, int srcIdx, int dstIdx, int cnt,
         boolean cpLeft) throws IgniteCheckedException {
+        assertPageType(dstPageAddr);
+
         assert srcIdx != dstIdx || srcPageAddr != dstPageAddr;
 
         cnt *= getItemSize() + 8; // From items to bytes.
@@ -139,6 +145,8 @@
         long rightId,
         boolean needRowBytes
     ) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         rowBytes = super.insert(pageAddr, idx, row, rowBytes, rightId, needRowBytes);
 
         // Setup reference to the right page on split.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusLeafIO.java
index e48d948..7ea2845 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusLeafIO.java
@@ -42,6 +42,7 @@
     @Override public final void copyItems(long srcPageAddr, long dstPageAddr, int srcIdx, int dstIdx, int cnt,
         boolean cpLeft) throws IgniteCheckedException {
         assert srcIdx != dstIdx || srcPageAddr != dstPageAddr;
+        assertPageType(dstPageAddr);
 
         PageHandler.copyMemory(srcPageAddr, offset(srcIdx), dstPageAddr, offset(dstIdx),
             cnt * getItemSize());
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
index 6bc0066..c05f072 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
@@ -104,6 +104,8 @@
      * @param pageSize Page size.
      */
     public void initRoot(long pageAdrr, long rootId, int pageSize) {
+        assertPageType(pageAdrr);
+
         setLevelsCount(pageAdrr, 1, pageSize);
         setFirstPageId(pageAdrr, 0, rootId);
     }
@@ -186,6 +188,8 @@
      * @param pageSize Page size.
      */
     public void addRoot(long pageAddr, long rootPageId, int pageSize) {
+        assertPageType(pageAddr);
+
         int lvl = getLevelsCount(pageAddr);
 
         setLevelsCount(pageAddr, lvl + 1, pageSize);
@@ -197,6 +201,8 @@
      * @param pageSize Page size.
      */
     public void cutRoot(long pageAddr, int pageSize) {
+        assertPageType(pageAddr);
+
         int lvl = getRootLevel(pageAddr);
 
         setLevelsCount(pageAddr, lvl, pageSize); // Decrease tree height.
@@ -207,6 +213,8 @@
      * @param size Offset size.
      */
     public void setInlineSize(long pageAddr, int size) {
+        assertPageType(pageAddr);
+
         if (getVersion() > 1)
             PageUtils.putShort(pageAddr, INLINE_SIZE_OFFSET, (short)size);
     }
@@ -260,6 +268,8 @@
      * @param createdVer The version of the product that creates the page (b+tree).
      */
     public void initFlagsAndVersion(long pageAddr, long flags, IgniteProductVersion createdVer) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, FLAGS_OFFSET, flags);
 
         setCreatedVersion(pageAddr, createdVer);
@@ -271,6 +281,7 @@
      */
     public void setCreatedVersion(long pageAddr, IgniteProductVersion curVer) {
         assert curVer != null;
+        assertPageType(pageAddr);
 
         PageUtils.putByte(pageAddr, CREATED_VER_OFFSET, curVer.major());
         PageUtils.putByte(pageAddr, CREATED_VER_OFFSET + 1, curVer.minor());
@@ -317,6 +328,7 @@
         boolean inlineObjSupported,
         boolean inlineObjHash) {
         assert supportFlags();
+        assertPageType(pageAddr);
 
         long flags = unwrappedPk ? FLAG_UNWRAPPED_PK : 0;
         flags |= inlineObjSupported ? FLAG_INLINE_OBJECT_SUPPORTED : 0;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/DataPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/DataPageIO.java
index bd0ded7..8718535 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/DataPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/DataPageIO.java
@@ -44,7 +44,7 @@
 public class DataPageIO extends AbstractDataPageIO<CacheDataRow> {
     /** */
     public static final int MVCC_INFO_SIZE = 40;
-    
+
     /** */
     public static final IOVersions<DataPageIO> VERSIONS = new IOVersions<>(
         new DataPageIO(1)
@@ -60,6 +60,8 @@
     /** {@inheritDoc} */
     @Override protected void writeRowData(long pageAddr, int dataOff, int payloadSize, CacheDataRow row,
         boolean newRow) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         long addr = pageAddr + dataOff;
 
         int cacheIdSize = row.cacheId() != 0 ? 4 : 0;
@@ -115,6 +117,8 @@
     /** {@inheritDoc} */
     @Override protected void writeFragmentData(CacheDataRow row, ByteBuffer buf, int rowOff,
         int payloadSize) throws IgniteCheckedException {
+        assertPageType(buf);
+
         final int keySize = row.key().valueBytesLength(null);
 
         final int valSize = row.value().valueBytesLength(null);
@@ -251,6 +255,8 @@
      * @param txState Tx state hint.
      */
     public void updateNewVersion(long pageAddr, int dataOff, long mvccCrd, long mvccCntr, int mvccOpCntr, byte txState) {
+        assertPageType(pageAddr);
+
         long addr = pageAddr + dataOff;
 
         updateNewVersion(addr, mvccCrd, mvccCntr,
@@ -266,6 +272,8 @@
      * @param mvccOpCntr Operation counter.
      */
     public void updateNewVersion(long pageAddr, int itemId, int pageSize, long mvccCrd, long mvccCntr, int mvccOpCntr) {
+        assertPageType(pageAddr);
+
         int dataOff = getDataOffset(pageAddr, itemId, pageSize);
 
         long addr = pageAddr + dataOff + (isFragmented(pageAddr, dataOff) ? 10 : 2);
@@ -280,6 +288,8 @@
      * @param txState Tx state hint.
      */
     public void updateTxState(long pageAddr, int itemId, int pageSize, byte txState) {
+        assertPageType(pageAddr);
+
         int dataOff = getDataOffset(pageAddr, itemId, pageSize);
 
         long addr = pageAddr + dataOff + (isFragmented(pageAddr, dataOff) ? 10 : 2);
@@ -296,6 +306,8 @@
      * @param txState Tx state hint.
      */
     public void updateNewTxState(long pageAddr, int itemId, int pageSize, byte txState) {
+        assertPageType(pageAddr);
+
         int dataOff = getDataOffset(pageAddr, itemId, pageSize);
 
         long addr = pageAddr + dataOff + (isFragmented(pageAddr, dataOff) ? 10 : 2);
@@ -368,6 +380,8 @@
      * @param opCntr MVCC counter value.
      */
     public void rawMvccOperationCounter(long pageAddr, int dataOff, int opCntr) {
+        assertPageType(pageAddr);
+
         long addr = pageAddr + dataOff;
 
         PageUtils.putInt(addr, 16, opCntr);
@@ -429,6 +443,8 @@
      * @param opCntr MVCC operation counter value.
      */
     public void rawNewMvccOperationCounter(long pageAddr, int dataOff, int opCntr) {
+        assertPageType(pageAddr);
+
         long addr = pageAddr + dataOff;
 
         // Skip xid_min.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
index cbc006c..bee16c3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
@@ -946,4 +946,22 @@
 
         return sb.toString();
     }
+
+    /**
+     * Asserts that page type of the page stored at pageAddr matches page type of this PageIO.
+     *
+     * @param pageAddr address of a page to use for assertion
+     */
+    protected final void assertPageType(long pageAddr) {
+        assert getType(pageAddr) == getType() : "Expected type " + getType() + ", but got " + getType(pageAddr);
+    }
+
+    /**
+     * Asserts that page type of the page stored in the given buffer matches page type of this PageIO.
+     *
+     * @param buf   buffer where the page for assertion is stored
+     */
+    protected final void assertPageType(ByteBuffer buf) {
+        assert getType(buf) == getType() : "Expected type " + getType() + ", but got " + getType(buf);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
index d12dd42..170d66d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
@@ -103,6 +103,8 @@
      * @param treeRoot Tree root
      */
     public void setTreeRoot(long pageAddr, long treeRoot) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, TREE_ROOT_OFF, treeRoot);
     }
 
@@ -119,6 +121,8 @@
      * @param pageId Root page ID.
      */
     public void setReuseListRoot(long pageAddr, long pageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, REUSE_LIST_ROOT_OFF, pageId);
     }
 
@@ -129,6 +133,8 @@
      */
     @Deprecated
     public void setLastSuccessfulSnapshotId(long pageAddr, long lastSuccessfulSnapshotId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, LAST_SUCCESSFUL_SNAPSHOT_ID_OFF, lastSuccessfulSnapshotId);
     }
 
@@ -148,6 +154,8 @@
      */
     @Deprecated
     public void setLastSuccessfulFullSnapshotId(long pageAddr, long lastSuccessfulFullSnapshotId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, LAST_SUCCESSFUL_FULL_SNAPSHOT_ID_OFF, lastSuccessfulFullSnapshotId);
     }
 
@@ -167,6 +175,8 @@
      */
     @Deprecated
     public void setNextSnapshotTag(long pageAddr, long nextSnapshotTag) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, NEXT_SNAPSHOT_TAG_OFF, nextSnapshotTag);
     }
 
@@ -186,6 +196,8 @@
      */
     @Deprecated
     public void setLastSuccessfulSnapshotTag(long pageAddr, long lastSuccessfulSnapshotTag) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, LAST_SUCCESSFUL_FULL_SNAPSHOT_TAG_OFF, lastSuccessfulSnapshotTag);
     }
 
@@ -205,6 +217,8 @@
      * @param pageCnt Last allocated pages count to set
      */
     public void setLastAllocatedPageCount(final long pageAddr, final int pageCnt) {
+        assertPageType(pageAddr);
+
         PageUtils.putInt(pageAddr, LAST_PAGE_COUNT_OFF, pageCnt);
     }
 
@@ -232,6 +246,8 @@
      * @param pageCnt Last page count.
      */
     public boolean setCandidatePageCount(long pageAddr, int pageCnt) {
+        assertPageType(pageAddr);
+
         if (getCandidatePageCount(pageAddr) == pageCnt)
             return false;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIOV2.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIOV2.java
index 6927c3e..84074aa 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIOV2.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIOV2.java
@@ -52,6 +52,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setEncryptedPageIndex(long pageAddr, int pageIdx) {
+        assertPageType(pageAddr);
+
         if (getEncryptedPageIndex(pageAddr) == pageIdx)
             return false;
 
@@ -75,6 +77,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setEncryptedPageCount(long pageAddr, int pagesCnt) {
+        assertPageType(pageAddr);
+
         if (getEncryptedPageCount(pageAddr) == pagesCnt)
             return false;
 
@@ -97,6 +101,8 @@
      * @param pageAddr Page address.
      */
     public void upgradePage(long pageAddr) {
+        assertPageType(pageAddr);
+
         assert PageIO.getType(pageAddr) == getType();
 
         PageIO.setVersion(pageAddr, getVersion());
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
index d16c5f8..b1adad7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
@@ -104,6 +104,8 @@
      * @param partMetaPageId Next counters page ID.
      */
     public void setNextCountersPageId(long pageAddr, long partMetaPageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, NEXT_COUNTERS_PAGE_OFF, partMetaPageId);
     }
 
@@ -116,6 +118,7 @@
     public int writeCacheSizes(int pageSize, long pageAddr, byte[] cacheSizes, int itemsOff) {
         assert cacheSizes != null;
         assert cacheSizes.length % ITEM_SIZE == 0 : cacheSizes.length;
+        assertPageType(pageAddr);
 
         int cap = getCapacity(pageSize);
 
@@ -179,6 +182,8 @@
      * @param last Last.
      */
     private void setLastFlag(long pageAddr, boolean last) {
+        assertPageType(pageAddr);
+
         PageUtils.putByte(pageAddr, LAST_FLAG_OFF, last ? LAST_FLAG : ~LAST_FLAG);
     }
 
@@ -196,6 +201,7 @@
      */
     private void setCount(long pageAddr, int cnt) {
         assert cnt >= 0 && cnt <= Short.MAX_VALUE : cnt;
+        assertPageType(pageAddr);
 
         PageUtils.putShort(pageAddr, CNT_OFF, (short)cnt);
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIO.java
index d2bb31c..4c859ca 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIO.java
@@ -86,6 +86,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setSize(long pageAddr, long size) {
+        assertPageType(pageAddr);
+
         if (getSize(pageAddr) == size)
             return false;
 
@@ -109,6 +111,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setUpdateCounter(long pageAddr, long cntr) {
+        assertPageType(pageAddr);
+
         if (getUpdateCounter(pageAddr) == cntr)
             return false;
 
@@ -132,6 +136,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setGlobalRemoveId(long pageAddr, long rmvId) {
+        assertPageType(pageAddr);
+
         if (getGlobalRemoveId(pageAddr) == rmvId)
             return false;
 
@@ -154,6 +160,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     public boolean setPartitionState(long pageAddr, byte state) {
+        assertPageType(pageAddr);
+
         if (getPartitionState(pageAddr) == state)
             return false;
 
@@ -179,6 +187,8 @@
      * @param cntrsPageId New cache sizes page ID.
      */
     public void setCountersPageId(long pageAddr, long cntrsPageId) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, NEXT_PART_META_PAGE_OFF, cntrsPageId);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV2.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV2.java
index 0d31dd5..6733adf 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV2.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV2.java
@@ -59,6 +59,8 @@
 
     /** {@inheritDoc} */
     @Override public void setPendingTreeRoot(long pageAddr, long listRoot) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, PENDING_TREE_ROOT_OFF, listRoot);
     }
 
@@ -74,6 +76,8 @@
      * @param listRoot List root.
      */
     @Override public void setPartitionMetaStoreReuseListRoot(long pageAddr, long listRoot) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, PART_META_REUSE_LIST_ROOT_OFF, listRoot);
     }
 
@@ -92,6 +96,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     @Override public boolean setGapsLink(long pageAddr, long link) {
+        assertPageType(pageAddr);
+
         if (getGapsLink(pageAddr) == link)
             return false;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV3.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV3.java
index c0c009f..53c2c32 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV3.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionMetaIOV3.java
@@ -62,6 +62,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     @Override public boolean setEncryptedPageIndex(long pageAddr, int pageIdx) {
+        assertPageType(pageAddr);
+
         if (getEncryptedPageIndex(pageAddr) == pageIdx)
             return false;
 
@@ -85,6 +87,8 @@
      * @return {@code true} if value has changed as a result of this method's invocation.
      */
     @Override public boolean setEncryptedPageCount(long pageAddr, int pagesCnt) {
+        assertPageType(pageAddr);
+
         if (getEncryptedPageCount(pageAddr) == pagesCnt)
             return false;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/SimpleDataPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/SimpleDataPageIO.java
index 716f8be..310c891 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/SimpleDataPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/SimpleDataPageIO.java
@@ -55,6 +55,8 @@
         final int rowOff,
         final int payloadSize
     ) throws IgniteCheckedException {
+        assertPageType(buf);
+
         int written = writeSizeFragment(row, buf, rowOff, payloadSize);
 
         if (payloadSize == written)
@@ -104,6 +106,8 @@
         SimpleDataRow row,
         boolean newRow
     ) throws IgniteCheckedException {
+        assertPageType(pageAddr);
+
         long addr = pageAddr + dataOff;
 
         if (newRow)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
index c850c15..8893d0e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
@@ -91,6 +91,8 @@
      * @return <code>-1</code> if everything is ok, otherwise last saved tag.
      */
     public long markChanged(ByteBuffer buf, long pageId, long nextSnapshotTag, long lastSuccessfulSnapshotTag, int pageSize) {
+        assertPageType(buf);
+
         long tag = validateSnapshotTag(buf, nextSnapshotTag, lastSuccessfulSnapshotTag, pageSize);
 
         int cntOfPage = countOfPageToTrack(pageSize);
@@ -266,6 +268,8 @@
      * @param buf Buffer.
      */
     public void resetCorruptFlag(ByteBuffer buf) {
+        assertPageType(buf);
+
         setLastSnasphotTag0(buf, getLastSnapshotTag(buf));
     }
 
@@ -275,6 +279,8 @@
      * @param addr Buffer.
      */
     public void resetCorruptFlag(long addr) {
+        assertPageType(addr);
+
         setLastSnasphotTag0(addr, getLastSnapshotTag(addr));
     }
 
@@ -291,6 +297,8 @@
      */
     public boolean wasChanged(ByteBuffer buf, long pageId, long curSnapshotTag, long lastSuccessfulSnapshotTag, int pageSize)
         throws TrackingPageIsCorruptedException {
+        assertPageType(buf);
+
         if (isCorrupted(buf))
             throw TrackingPageIsCorruptedException.INSTANCE;
 
@@ -340,10 +348,10 @@
     /**
      * @param snapshotTag Snapshot id.
      *
-     * @return true if snapshotTag is odd, otherwise - false.
+     * @return true if snapshotTag is even, otherwise - false.
      */
     private boolean useLeftHalf(long snapshotTag) {
-        return (snapshotTag & 0b1) == 0;
+        return (snapshotTag & 1) == 0;
     }
 
     /**
@@ -388,6 +396,8 @@
      */
     @Nullable public Long findNextChangedPage(ByteBuffer buf, long start, long curSnapshotTag,
         long lastSuccessfulSnapshotTag, int pageSize) throws TrackingPageIsCorruptedException {
+        assertPageType(buf);
+
         if (isCorrupted(buf))
             throw TrackingPageIsCorruptedException.INSTANCE;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/AbstractWalRecordsIterator.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/AbstractWalRecordsIterator.java
index 0ff2694..926bce8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/AbstractWalRecordsIterator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/AbstractWalRecordsIterator.java
@@ -168,15 +168,19 @@
      * @throws IgniteCheckedException If failed.
      */
     protected void advance() throws IgniteCheckedException {
+        if (curRec != null)
+            lastRead = curRec.get1();
+
         while (true) {
             try {
                 curRec = advanceRecord(currWalSegment);
 
                 if (curRec != null) {
-                    lastRead = curRec.get1();
+                    if (curRec.get2().type() == null) {
+                        lastRead = curRec.get1();
 
-                    if (curRec.get2().type() == null)
                         continue; // Record was skipped by filter of current serializer, should read next record.
+                    }
 
                     return;
                 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/filehandle/FileWriteHandleImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/filehandle/FileWriteHandleImpl.java
index 6e088a2..aacb5b3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/filehandle/FileWriteHandleImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/filehandle/FileWriteHandleImpl.java
@@ -498,7 +498,20 @@
 
                             switchSegmentRecordOffset = segRecPtr.fileOffset() + switchSegmentRecSize;
                         }
+                        else {
+                            if (log.isDebugEnabled())
+                                log.debug("Not enough space in wal segment to write segment switch");
+                        }
                     }
+                    else {
+                        if (log.isDebugEnabled()) {
+                            log.debug("Not enough space in wal segment to write segment switch, written="
+                                + written + ", switchSegmentRecSize=" + switchSegmentRecSize);
+                        }
+                    }
+
+                    // Unconditional flush (tail of the buffer)
+                    flushOrWait(null);
 
                     if (mmap) {
                         List<SegmentedRingByteBuffer.ReadSegment> segs = buf.poll(maxWalSegmentSize);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIterator.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIterator.java
index 14e99bc..051be36 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIterator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIterator.java
@@ -406,11 +406,11 @@
         final CacheObjectContext fakeCacheObjCtx = new CacheObjectContext(
             kernalCtx, null, null, false, false, false, false, false);
 
-        final List<DataEntry> entries = dataRec.writeEntries();
-        final List<DataEntry> postProcessedEntries = new ArrayList<>(entries.size());
+        final int entryCnt = dataRec.entryCount();
+        final List<DataEntry> postProcessedEntries = new ArrayList<>(entryCnt);
 
-        for (DataEntry dataEntry : entries) {
-            final DataEntry postProcessedEntry = postProcessDataEntry(processor, fakeCacheObjCtx, dataEntry);
+        for (int i = 0; i < entryCnt; i++) {
+            final DataEntry postProcessedEntry = postProcessDataEntry(processor, fakeCacheObjCtx, dataRec.get(i));
 
             postProcessedEntries.add(postProcessedEntry);
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV1Serializer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV1Serializer.java
index c99c97e..81ddabb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV1Serializer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV1Serializer.java
@@ -681,12 +681,16 @@
             case DATA_RECORD_V2:
                 int entryCnt = in.readInt();
 
-                List<DataEntry> entries = new ArrayList<>(entryCnt);
+                if (entryCnt == 1)
+                    res = new DataRecord(readPlainDataEntry(in, type), 0L);
+                else {
+                    List<DataEntry> entries = new ArrayList<>(entryCnt);
 
-                for (int i = 0; i < entryCnt; i++)
-                    entries.add(readPlainDataEntry(in, type));
+                    for (int i = 0; i < entryCnt; i++)
+                        entries.add(readPlainDataEntry(in, type));
 
-                res = new DataRecord(entries, 0L);
+                    res = new DataRecord(entries, 0L);
+                }
 
                 break;
 
@@ -695,12 +699,16 @@
             case ENCRYPTED_DATA_RECORD_V3:
                 entryCnt = in.readInt();
 
-                entries = new ArrayList<>(entryCnt);
+                if (entryCnt == 1)
+                    res = new DataRecord(readEncryptedDataEntry(in, type), 0L);
+                else {
+                    List<DataEntry> entries = new ArrayList<>(entryCnt);
 
-                for (int i = 0; i < entryCnt; i++)
-                    entries.add(readEncryptedDataEntry(in, type));
+                    for (int i = 0; i < entryCnt; i++)
+                        entries.add(readEncryptedDataEntry(in, type));
 
-                res = new DataRecord(entries, 0L);
+                    res = new DataRecord(entries, 0L);
+                }
 
                 break;
 
@@ -1381,15 +1389,17 @@
             case DATA_RECORD_V2:
                 DataRecord dataRec = (DataRecord)rec;
 
-                buf.putInt(dataRec.writeEntries().size());
+                int entryCnt = dataRec.entryCount();
+
+                buf.putInt(entryCnt);
 
                 boolean encrypted = isDataRecordEncrypted(dataRec);
 
-                for (DataEntry dataEntry : dataRec.writeEntries()) {
+                for (int i = 0; i < entryCnt; i++) {
                     if (encrypted)
-                        putEncryptedDataEntry(buf, dataEntry);
+                        putEncryptedDataEntry(buf, dataRec.get(i));
                     else
-                        putPlainDataEntry(buf, dataEntry);
+                        putPlainDataEntry(buf, dataRec.get(i));
                 }
 
                 break;
@@ -2180,7 +2190,11 @@
         if (encryptionDisabled)
             return false;
 
-        for (DataEntry e : rec.writeEntries()) {
+        int entryCnt = rec.entryCount();
+
+        for (int i = 0; i < entryCnt; i++) {
+            DataEntry e = rec.get(i);
+
             if (cctx.cacheContext(e.cacheId()) != null && needEncryption(cctx.cacheContext(e.cacheId()).groupId()))
                 return true;
         }
@@ -2253,8 +2267,11 @@
         boolean encrypted = isDataRecordEncrypted(dataRec);
 
         int sz = 0;
+        int entryCnt = dataRec.entryCount();
 
-        for (DataEntry entry : dataRec.writeEntries()) {
+        for (int i = 0; i < entryCnt; i++) {
+            DataEntry entry = dataRec.get(i);
+
             int clSz = entrySize(entry);
 
             if (!encryptionDisabled && needEncryption(cctx.cacheContext(entry.cacheId()).groupId()))
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV2Serializer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV2Serializer.java
index 2637d93..62fc50ef 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV2Serializer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/serializer/RecordDataV2Serializer.java
@@ -165,18 +165,22 @@
                 int entryCnt = in.readInt();
                 long timeStamp = in.readLong();
 
-                List<DataEntry> entries = new ArrayList<>(entryCnt);
+                if (entryCnt == 1)
+                    return new DataRecord(readPlainDataEntry(in, type), timeStamp);
+                else {
+                    List<DataEntry> entries = new ArrayList<>(entryCnt);
 
-                for (int i = 0; i < entryCnt; i++)
-                    entries.add(readPlainDataEntry(in, type));
+                    for (int i = 0; i < entryCnt; i++)
+                        entries.add(readPlainDataEntry(in, type));
 
-                return new DataRecord(entries, timeStamp);
+                    return new DataRecord(entries, timeStamp);
+                }
 
             case MVCC_DATA_RECORD:
                 entryCnt = in.readInt();
                 timeStamp = in.readLong();
 
-                entries = new ArrayList<>(entryCnt);
+                List<DataEntry> entries = new ArrayList<>(entryCnt);
 
                 for (int i = 0; i < entryCnt; i++)
                     entries.add(readMvccDataEntry(in));
@@ -189,12 +193,16 @@
                 entryCnt = in.readInt();
                 timeStamp = in.readLong();
 
-                entries = new ArrayList<>(entryCnt);
+                if (entryCnt == 1)
+                    return new DataRecord(readEncryptedDataEntry(in, type), timeStamp);
+                else {
+                    entries = new ArrayList<>(entryCnt);
 
-                for (int i = 0; i < entryCnt; i++)
-                    entries.add(readEncryptedDataEntry(in, type));
+                    for (int i = 0; i < entryCnt; i++)
+                        entries.add(readEncryptedDataEntry(in, type));
 
-                return new DataRecord(entries, timeStamp);
+                    return new DataRecord(entries, timeStamp);
+                }
 
             case SNAPSHOT:
                 long snpId = in.readLong();
@@ -266,12 +274,16 @@
             case DATA_RECORD_V2:
                 DataRecord dataRec = (DataRecord)rec;
 
-                buf.putInt(dataRec.writeEntries().size());
+                int entryCnt = dataRec.entryCount();
+
+                buf.putInt(entryCnt);
                 buf.putLong(dataRec.timestamp());
 
                 boolean encrypted = isDataRecordEncrypted(dataRec);
 
-                for (DataEntry dataEntry : dataRec.writeEntries()) {
+                for (int i = 0; i < entryCnt; i++) {
+                    DataEntry dataEntry = dataRec.get(i);
+
                     if (encrypted)
                         putEncryptedDataEntry(buf, dataEntry);
                     else
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheSqlQuery.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheSqlQuery.java
index 9a837bb..4c1d8c2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheSqlQuery.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheSqlQuery.java
@@ -82,7 +82,7 @@
     /** Flag indicating that the query contains an OUTER JOIN from REPLICATED to PARTITIONED. */
     @GridToStringInclude
     @GridDirectTransient
-    private transient boolean hasOuterJoinReplicatedPartitioned;
+    private transient boolean treatPartitionedAsReplicated;
 
     /**
      * For {@link Message}.
@@ -375,19 +375,23 @@
     }
 
     /**
-     * @return {@code true} if the query contains an OUTER JOIN from REPLICATED to PARTITIONED.
+     * @return {@code true} if the query contains an OUTER JOIN from REPLICATED to PARTITIONED, or
+     * outer query over REPLICATED cache has a subquery over PARTIITIONED.
      */
-    public boolean hasOuterJoinReplicatedPartitioned() {
-        return hasOuterJoinReplicatedPartitioned;
+    public boolean treatReplicatedAsPartitioned() {
+        return treatPartitionedAsReplicated;
     }
 
     /**
-     * @param hasOuterJoinReplicatedPartitioned Flag indicating that the query contains an OUTER JOIN from REPLICATED to PARTITIONED.
+     * Set flag to {@code true} when query contains an OUTER JOIN from REPLICATED to PARTITIONED, or
+     * outer query over REPLICATED cache has a subquery over PARTIITIONED.
      *
+     * @param trearPartitionedAsReplicated Flag indicating that the replicated cache in outer query must be treat
+     * as partitioned.
      * @return {@code this}.
      */
-    public GridCacheSqlQuery hasOuterJoinReplicatedPartitioned(boolean hasOuterJoinReplicatedPartitioned) {
-        this.hasOuterJoinReplicatedPartitioned = hasOuterJoinReplicatedPartitioned;
+    public GridCacheSqlQuery treatReplicatedAsPartitioned(boolean trearPartitionedAsReplicated) {
+        this.treatPartitionedAsReplicated = trearPartitionedAsReplicated;
 
         return this;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
index 75ac0c4..67aeb1d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
@@ -96,7 +96,7 @@
         private final IndexQueryResultMeta meta;
 
         /** Every node will return the same key types for the same index, then it's possible to use simple comparator. */
-        private final IndexRowComparator idxRowComp = new IndexRowCompartorImpl();
+        private final IndexRowComparator idxRowComp = new IndexRowCompartorImpl(null);
 
         /** */
         IndexedNodePageComparator(
@@ -138,12 +138,29 @@
 
         /** */
         private IndexKey key(String key, int type, IgniteBiTuple<?, ?> entry) throws IgniteCheckedException {
-            GridQueryProperty prop = typeDesc.property(key);
+            Object o;
 
-            // PrimaryKey field.
-            Object o = prop == null ? entry.getKey() : prop.value(entry.getKey(), entry.getValue());
+            if (isKeyField(key))
+                o = entry.getKey();
+            else if (isValueField(key))
+                o = entry.getValue();
+            else {
+                GridQueryProperty prop = typeDesc.property(key);
+
+                o = prop.value(entry.getKey(), entry.getValue());
+            }
 
             return IndexKeyFactory.wrap(o, type, cctx.cacheObjectContext(), meta.keyTypeSettings());
         }
+
+        /** */
+        private boolean isKeyField(String fld) {
+            return fld.equals(typeDesc.keyFieldAlias()) || fld.equals(QueryUtils.KEY_FIELD_NAME);
+        }
+
+        /** */
+        private boolean isValueField(String fld) {
+            return fld.equals(typeDesc.valueFieldAlias()) || fld.equals(QueryUtils.VAL_FIELD_NAME);
+        }
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataInnerIO.java
index 896b3a9..f2d1c82 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataInnerIO.java
@@ -47,6 +47,7 @@
     /** {@inheritDoc} */
     @Override public final void storeByOffset(long pageAddr, int off, CacheSearchRow row) {
         assert row.link() != 0;
+        assertPageType(pageAddr);
 
         PageUtils.putLong(pageAddr, off, row.link());
         off += 8;
@@ -112,6 +113,8 @@
         long srcPageAddr,
         int srcIdx)
     {
+        assertPageType(dstPageAddr);
+
         RowLinkIO rowIo = ((RowLinkIO)srcIo);
 
         long link = rowIo.getLink(srcPageAddr, srcIdx);
@@ -165,6 +168,8 @@
 
     /** {@inheritDoc} */
     @Override public void visit(long pageAddr, IgniteInClosure<CacheSearchRow> c) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         for (int i = 0; i < cnt; i++)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataLeafIO.java
index 0036bb2f..eea4aa9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractDataLeafIO.java
@@ -45,6 +45,7 @@
     /** {@inheritDoc} */
     @Override public void storeByOffset(long pageAddr, int off, CacheSearchRow row) {
         assert row.link() != 0;
+        assertPageType(pageAddr);
 
         PageUtils.putLong(pageAddr, off, row.link());
         off += 8;
@@ -87,6 +88,8 @@
     /** {@inheritDoc} */
     @Override public void store(long dstPageAddr, int dstIdx, BPlusIO<CacheSearchRow> srcIo, long srcPageAddr,
         int srcIdx) {
+        assertPageType(dstPageAddr);
+
         RowLinkIO rowIo = (RowLinkIO)srcIo;
 
         long link = rowIo.getLink(srcPageAddr, srcIdx);
@@ -179,6 +182,8 @@
 
     /** {@inheritDoc} */
     @Override public void visit(long pageAddr, IgniteInClosure<CacheSearchRow> c) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         for (int i = 0; i < cnt; i++)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryInnerIO.java
index 1ba0472..74fe3bf 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryInnerIO.java
@@ -42,6 +42,7 @@
     @Override public void storeByOffset(long pageAddr, int off, PendingRow row) throws IgniteCheckedException {
         assert row.link != 0;
         assert row.expireTime != 0;
+        assertPageType(pageAddr);
 
         PageUtils.putLong(pageAddr, off, row.expireTime);
         PageUtils.putLong(pageAddr, off + 8, row.link);
@@ -59,6 +60,8 @@
         BPlusIO<PendingRow> srcIo,
         long srcPageAddr,
         int srcIdx) throws IgniteCheckedException {
+        assertPageType(dstPageAddr);
+
         int dstOff = offset(dstIdx);
 
         long link = ((PendingRowIO)srcIo).getLink(srcPageAddr, srcIdx);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryLeafIO.java
index b64b2e9..111a8e7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/AbstractPendingEntryLeafIO.java
@@ -41,6 +41,7 @@
     @Override public void storeByOffset(long pageAddr, int off, PendingRow row) throws IgniteCheckedException {
         assert row.link != 0;
         assert row.expireTime != 0;
+        assertPageType(pageAddr);
 
         PageUtils.putLong(pageAddr, off, row.expireTime);
         PageUtils.putLong(pageAddr, off + 8, row.link);
@@ -58,6 +59,8 @@
         BPlusIO<PendingRow> srcIo,
         long srcPageAddr,
         int srcIdx) throws IgniteCheckedException {
+        assertPageType(dstPageAddr);
+
         int dstOff = offset(dstIdx);
 
         long link = ((PendingRowIO)srcIo).getLink(srcPageAddr, srcIdx);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccCacheIdAwareDataLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccCacheIdAwareDataLeafIO.java
index 91b149c..95ed5f3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccCacheIdAwareDataLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccCacheIdAwareDataLeafIO.java
@@ -79,11 +79,15 @@
 
     /** {@inheritDoc} */
     @Override public void setMvccLockCoordinatorVersion(long pageAddr, int idx, long lockCrd) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset(idx) + 36, lockCrd);
     }
 
     /** {@inheritDoc} */
     @Override public void setMvccLockCounter(long pageAddr, int idx, long lockCntr) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset(idx) + 44, lockCntr);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataInnerIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataInnerIO.java
index 8856027..6ee3bd5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataInnerIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataInnerIO.java
@@ -41,6 +41,8 @@
 
     /** {@inheritDoc} */
     @Override public void visit(long pageAddr, IgniteInClosure<CacheSearchRow> c) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         for (int i = 0; i < cnt; i++)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataLeafIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataLeafIO.java
index ab498d9..803a829 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataLeafIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/tree/mvcc/data/MvccDataLeafIO.java
@@ -41,6 +41,8 @@
 
     /** {@inheritDoc} */
     @Override public void visit(long pageAddr, IgniteInClosure<CacheSearchRow> c) {
+        assertPageType(pageAddr);
+
         int cnt = getCount(pageAddr);
 
         for (int i = 0; i < cnt; i++)
@@ -79,11 +81,15 @@
 
     /** {@inheritDoc} */
     @Override public void setMvccLockCoordinatorVersion(long pageAddr, int idx, long lockCrd) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset(idx) + 32, lockCrd);
     }
 
     /** {@inheritDoc} */
     @Override public void setMvccLockCounter(long pageAddr, int idx, long lockCntr) {
+        assertPageType(pageAddr);
+
         PageUtils.putLong(pageAddr, offset(idx) + 40, lockCntr);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersion.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersion.java
index 7769501..a54a9f5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersion.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersion.java
@@ -326,6 +326,7 @@
     @Override public String toString() {
         return "GridCacheVersion [topVer=" + topologyVersion() +
             ", order=" + order() +
-            ", nodeOrder=" + nodeOrder() + ']';
+            ", nodeOrder=" + nodeOrder() +
+            ", dataCenterId=" + dataCenterId() + ']';
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionEx.java
index 1d5bad0..4848893 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionEx.java
@@ -168,6 +168,7 @@
         return "GridCacheVersionEx [topVer=" + topologyVersion() +
             ", order=" + order() +
             ", nodeOrder=" + nodeOrder() +
+            ", dataCenterId=" + dataCenterId() +
             ", drVer=" + drVer + ']';
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionManager.java
index 48a6507..ac81bf9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/version/GridCacheVersionManager.java
@@ -49,6 +49,9 @@
     /** Last data version metric name. */
     public static final String LAST_DATA_VER = "LastDataVersion";
 
+    /** Cluster ID metric name. */
+    public static final String DATA_VER_CLUSTER_ID = "DataVersionClusterId";
+
     /** Last version metric. */
     protected AtomicLongMetric lastDataVer;
 
@@ -99,6 +102,8 @@
 
         lastDataVer = sysreg.longMetric(LAST_DATA_VER, "The latest data version on the node.");
 
+        sysreg.register(DATA_VER_CLUSTER_ID, () -> dataCenterId, "Data version cluster id.");
+
         startVer = new GridCacheVersion(0, 0, 0, dataCenterId);
 
         cctx.gridEvents().addLocalEventListener(discoLsnr, EVT_NODE_METRICS_UPDATED);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
index 4ef20a7..caa7855 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
@@ -1258,7 +1258,7 @@
         DiscoveryDataClusterState joiningNodeState;
 
         try {
-            joiningNodeState = marsh.unmarshal((byte[])discoData.joiningNodeData(), Thread.currentThread().getContextClassLoader());
+            joiningNodeState = marsh.unmarshal((byte[])discoData.joiningNodeData(), U.resolveClassLoader(ctx.config()));
         }
         catch (IgniteCheckedException e) {
             String msg = "Error on unmarshalling discovery data " +
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheQueueAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheQueueAdapter.java
index 3a0b09f..a4e761a0e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheQueueAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheQueueAdapter.java
@@ -441,7 +441,7 @@
             false,
             null,
             false,
-            false,
+            null,
             DFLT_ALLOW_ATOMIC_OPS_IN_TX)
             : opCtx.keepBinary();
 
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/ComputeJobStatusEnum.java
similarity index 74%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/core/src/main/java/org/apache/ignite/internal/processors/job/ComputeJobStatusEnum.java
index a5b6d8f..a700535 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/ComputeJobStatusEnum.java
@@ -15,18 +15,27 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+package org.apache.ignite.internal.processors.job;
 
 /**
- * Zookeeper IP Finder tests.
+ * Compute job status.
  */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
+public enum ComputeJobStatusEnum {
+    /** */
+    QUEUED,
 
+    /** */
+    RUNNING,
+
+    /** */
+    SUSPENDED,
+
+    /** */
+    FAILED,
+
+    /** */
+    CANCELLED,
+
+    /** */
+    FINISHED;
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobEventListener.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobEventListener.java
index 3eed487..32af38b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobEventListener.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobEventListener.java
@@ -24,17 +24,22 @@
  */
 interface GridJobEventListener extends EventListener {
     /**
+     * @param worker Job worker.
+     */
+    void onJobQueued(GridJobWorker worker);
+
+    /**
      * @param worker Started job worker.
      */
-    public void onJobStarted(GridJobWorker worker);
+    void onJobStarted(GridJobWorker worker);
 
     /**
      * @param worker Job worker.
      */
-    public void onBeforeJobResponseSent(GridJobWorker worker);
+    void onBeforeJobResponseSent(GridJobWorker worker);
 
     /**
      * @param worker Finished job worker.
      */
-    public void onJobFinished(GridJobWorker worker);
+    void onJobFinished(GridJobWorker worker);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobProcessor.java
index f47db23..122b2ea 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobProcessor.java
@@ -36,6 +36,8 @@
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteDeploymentException;
 import org.apache.ignite.IgniteException;
@@ -61,6 +63,7 @@
 import org.apache.ignite.internal.SkipDaemon;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.managers.collision.GridCollisionJobContextAdapter;
+import org.apache.ignite.internal.managers.collision.GridCollisionManager;
 import org.apache.ignite.internal.managers.communication.GridIoManager;
 import org.apache.ignite.internal.managers.communication.GridMessageListener;
 import org.apache.ignite.internal.managers.deployment.GridDeployment;
@@ -80,7 +83,6 @@
 import org.apache.ignite.internal.util.GridConcurrentHashSet;
 import org.apache.ignite.internal.util.GridSpinReadWriteLock;
 import org.apache.ignite.internal.util.typedef.F;
-import org.apache.ignite.internal.util.typedef.P1;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.LT;
 import org.apache.ignite.internal.util.typedef.internal.S;
@@ -89,6 +91,8 @@
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.lang.IgniteUuid;
 import org.apache.ignite.marshaller.Marshaller;
+import org.apache.ignite.spi.collision.CollisionSpi;
+import org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi;
 import org.apache.ignite.spi.metric.DoubleMetric;
 import org.apache.ignite.spi.systemview.view.ComputeJobView;
 import org.apache.ignite.spi.systemview.view.ComputeJobView.ComputeJobState;
@@ -97,6 +101,8 @@
 import org.jsr166.ConcurrentLinkedHashMap;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.stream.Collectors.counting;
+import static java.util.stream.Collectors.groupingBy;
 import static org.apache.ignite.IgniteSystemProperties.IGNITE_JOBS_HISTORY_SIZE;
 import static org.apache.ignite.events.EventType.EVT_JOB_FAILED;
 import static org.apache.ignite.events.EventType.EVT_NODE_FAILED;
@@ -163,7 +169,7 @@
     /** */
     private final Marshaller marsh;
 
-    /** */
+    /** Collision SPI is not available: {@link GridCollisionManager#enabled()} {@code == false}. */
     private final boolean jobAlwaysActivate;
 
     /** */
@@ -295,6 +301,18 @@
     private final ThreadLocal<GridJobSessionImpl> currSess = new ThreadLocal<>();
 
     /**
+     * {@link PriorityQueueCollisionSpi#getPriorityAttributeKey} or
+     * {@link null} if the {@link PriorityQueueCollisionSpi} is not configured.
+     */
+    @Nullable private final String taskPriAttrKey;
+
+    /**
+     * {@link PriorityQueueCollisionSpi#getJobPriorityAttributeKey} or
+     * {@link null} if the {@link PriorityQueueCollisionSpi} is not configured.
+     */
+    @Nullable private final String jobPriAttrKey;
+
+    /**
      * @param ctx Kernal context.
      */
     public GridJobProcessor(GridKernalContext ctx) {
@@ -349,6 +367,17 @@
 
                 return new ComputeJobView(e.getKey(), e.getValue(), state);
             });
+
+        CollisionSpi collisionSpi = ctx.config().getCollisionSpi();
+
+        if (!jobAlwaysActivate && collisionSpi instanceof PriorityQueueCollisionSpi) {
+            taskPriAttrKey = ((PriorityQueueCollisionSpi)collisionSpi).getPriorityAttributeKey();
+            jobPriAttrKey = ((PriorityQueueCollisionSpi)collisionSpi).getJobPriorityAttributeKey();
+        }
+        else {
+            taskPriAttrKey = null;
+            jobPriAttrKey = null;
+        }
     }
 
     /** {@inheritDoc} */
@@ -737,26 +766,18 @@
             // Put either job ID or session ID (they are unique).
             cancelReqs.putIfAbsent(jobId != null ? jobId : sesId, sys);
 
-            IgnitePredicate<GridJobWorker> idsMatch = new P1<GridJobWorker>() {
-                @Override public boolean apply(GridJobWorker e) {
-                    return sesId != null ?
-                        jobId != null ?
-                            e.getSession().getId().equals(sesId) && e.getJobId().equals(jobId) :
-                            e.getSession().getId().equals(sesId) :
-                        e.getJobId().equals(jobId);
-                }
-            };
+            Predicate<GridJobWorker> idsMatch = idMatch(sesId, jobId);
 
             // If we don't have jobId then we have to iterate
             if (jobId == null) {
                 if (!jobAlwaysActivate) {
                     for (GridJobWorker job : passiveJobs.values()) {
-                        if (idsMatch.apply(job))
+                        if (idsMatch.test(job))
                             cancelPassiveJob(job);
                     }
                 }
                 for (GridJobWorker job : activeJobs.values()) {
-                    if (idsMatch.apply(job))
+                    if (idsMatch.test(job))
                         cancelActiveJob(job, sys);
                 }
             }
@@ -764,13 +785,13 @@
                 if (!jobAlwaysActivate) {
                     GridJobWorker passiveJob = passiveJobs.get(jobId);
 
-                    if (passiveJob != null && idsMatch.apply(passiveJob) && cancelPassiveJob(passiveJob))
+                    if (passiveJob != null && idsMatch.test(passiveJob) && cancelPassiveJob(passiveJob))
                         return;
                 }
 
                 GridJobWorker activeJob = activeJobs.get(jobId);
 
-                if (activeJob != null && idsMatch.apply(activeJob))
+                if (activeJob != null && idsMatch.test(activeJob))
                     cancelActiveJob(activeJob, sys);
             }
         }
@@ -877,24 +898,29 @@
             ctx.collision().onCollision(
                 // Passive jobs view.
                 new AbstractCollection<org.apache.ignite.spi.collision.CollisionJobContext>() {
+                    /** {@inheritDoc} */
                     @NotNull @Override public Iterator<org.apache.ignite.spi.collision.CollisionJobContext> iterator() {
                         final Iterator<GridJobWorker> iter = passiveJobs.values().iterator();
 
                         return new Iterator<org.apache.ignite.spi.collision.CollisionJobContext>() {
+                            /** {@inheritDoc} */
                             @Override public boolean hasNext() {
                                 return iter.hasNext();
                             }
 
+                            /** {@inheritDoc} */
                             @Override public org.apache.ignite.spi.collision.CollisionJobContext next() {
                                 return new CollisionJobContext(iter.next(), true);
                             }
 
+                            /** {@inheritDoc} */
                             @Override public void remove() {
                                 throw new UnsupportedOperationException();
                             }
                         };
                     }
 
+                    /** {@inheritDoc} */
                     @Override public int size() {
                         return passiveJobs.size();
                     }
@@ -902,6 +928,7 @@
 
                 // Active jobs view.
                 new AbstractCollection<org.apache.ignite.spi.collision.CollisionJobContext>() {
+                    /** {@inheritDoc} */
                     @NotNull @Override public Iterator<org.apache.ignite.spi.collision.CollisionJobContext> iterator() {
                         final Iterator<GridJobWorker> iter = activeJobs.values().iterator();
 
@@ -931,10 +958,12 @@
                                 }
                             }
 
+                            /** {@inheritDoc} */
                             @Override public boolean hasNext() {
                                 return w != null;
                             }
 
+                            /** {@inheritDoc} */
                             @Override public org.apache.ignite.spi.collision.CollisionJobContext next() {
                                 if (w == null)
                                     throw new NoSuchElementException();
@@ -948,21 +977,24 @@
                                 return ret;
                             }
 
+                            /** {@inheritDoc} */
                             @Override public void remove() {
                                 throw new UnsupportedOperationException();
                             }
                         };
                     }
 
+                    /** {@inheritDoc} */
                     @Override public int size() {
                         int ret = activeJobs.size() - heldJobs.size();
 
-                        return ret > 0 ? ret : 0;
+                        return Math.max(ret, 0);
                     }
                 },
 
                 // Held jobs view.
                 new AbstractCollection<org.apache.ignite.spi.collision.CollisionJobContext>() {
+                    /** {@inheritDoc} */
                     @NotNull @Override public Iterator<org.apache.ignite.spi.collision.CollisionJobContext> iterator() {
                         final Iterator<GridJobWorker> iter = activeJobs.values().iterator();
 
@@ -992,10 +1024,12 @@
                                 }
                             }
 
+                            /** {@inheritDoc} */
                             @Override public boolean hasNext() {
                                 return w != null;
                             }
 
+                            /** {@inheritDoc} */
                             @Override public org.apache.ignite.spi.collision.CollisionJobContext next() {
                                 if (w == null)
                                     throw new NoSuchElementException();
@@ -1010,12 +1044,14 @@
                                 return ret;
                             }
 
+                            /** {@inheritDoc} */
                             @Override public void remove() {
                                 throw new UnsupportedOperationException();
                             }
                         };
                     }
 
+                    /** {@inheritDoc} */
                     @Override public int size() {
                         return heldJobs.size();
                     }
@@ -1248,7 +1284,9 @@
                             sesAttrs,
                             req.isSessionFullSupport(),
                             req.isInternal(),
-                            req.executorName());
+                            req.executorName(),
+                            ctx.security().enabled() ? ctx.security().securityContext().subject().login() : null
+                        );
 
                         taskSes.setCheckpointSpi(req.getCheckpointSpi());
                         taskSes.setClassLoader(dep.classLoader());
@@ -1691,6 +1729,8 @@
             synchronized (ses) {
                 ses.setInternal(attrs);
             }
+
+            onChangeTaskAttributes(req.getSessionId(), req.getJobId(), attrs);
         }
         catch (IgniteCheckedException e) {
             U.error(log, "Failed to deserialize session attributes.", e);
@@ -1947,6 +1987,14 @@
         private final GridMessageListener sesLsnr = new JobSessionListener();
 
         /** {@inheritDoc} */
+        @Override public void onJobQueued(GridJobWorker worker) {
+            if (worker.getSession().isFullSupport()) {
+                // Register session request listener for this job.
+                ctx.io().addMessageListener(worker.getJobTopic(), sesLsnr);
+            }
+        }
+
+        /** {@inheritDoc} */
         @Override public void onJobStarted(GridJobWorker worker) {
             if (log.isDebugEnabled())
                 log.debug("Received onJobStarted() callback: " + worker);
@@ -1957,10 +2005,6 @@
             // Register for timeout notifications.
             if (worker.endTime() < Long.MAX_VALUE)
                 ctx.timeout().addTimeoutObject(worker);
-
-            if (worker.getSession().isFullSupport())
-                // Register session request listener for this job.
-                ctx.io().addMessageListener(worker.getJobTopic(), sesLsnr);
         }
 
         /** {@inheritDoc} */
@@ -2295,4 +2339,79 @@
             return sizex();
         }
     }
+
+    /**
+     * Callback on changing task attributes.
+     *
+     * @param sesId Session ID.
+     * @param jobId Job ID.
+     * @param attrs Changed attributes.
+     */
+    public void onChangeTaskAttributes(IgniteUuid sesId, IgniteUuid jobId, Map<?, ?> attrs) {
+        if (!rwLock.tryReadLock()) {
+            if (log.isDebugEnabled())
+                log.debug("Callback on changing the task attributes will be ignored " +
+                    "(node is in the process of stopping): " + sesId);
+
+            return;
+        }
+
+        try {
+            if (jobAlwaysActivate || (taskPriAttrKey == null && jobPriAttrKey == null))
+                return;
+
+            GridJobWorker jobWorker = passiveJobs.get(jobId);
+
+            if (jobWorker == null || jobWorker.isInternal())
+                return;
+
+            boolean handleCollisions = false;
+
+            if (taskPriAttrKey != null && attrs.containsKey(taskPriAttrKey)) {
+                // See PriorityQueueCollisionSpi#bumpPriority.
+                jobWorker.getSession().setAttribute(jobPriAttrKey, attrs.get(taskPriAttrKey));
+
+                handleCollisions = true;
+            }
+
+            if (!handleCollisions && jobPriAttrKey != null && attrs.containsKey(jobPriAttrKey))
+                handleCollisions = true;
+
+            if (handleCollisions)
+                handleCollisions();
+        }
+        finally {
+            rwLock.readUnlock();
+        }
+    }
+
+    /**
+     * @param sesId Task session ID.
+     * @return Job statistics for the task. Mapping: Job status -> count of jobs.
+     */
+    public Map<ComputeJobStatusEnum, Long> jobStatuses(IgniteUuid sesId) {
+        return Stream.concat(
+                activeJobs.values().stream(),
+                jobAlwaysActivate ? cancelledJobs.values().stream() :
+                    Stream.concat(passiveJobs.values().stream(), cancelledJobs.values().stream())
+            )
+            .filter(idMatch(sesId, null))
+            .collect(groupingBy(GridJobWorker::status, counting()));
+    }
+
+    /**
+     * @param sesId Task session ID.
+     * @param jobId Job ID.
+     * @return ID workers predicate.
+     */
+    private Predicate<GridJobWorker> idMatch(@Nullable IgniteUuid sesId, @Nullable IgniteUuid jobId) {
+        assert sesId != null || jobId != null;
+
+        if (sesId == null)
+            return w -> jobId.equals(w.getJobId());
+        else if (jobId == null)
+            return w -> sesId.equals(w.getSession().getId());
+        else
+            return w -> sesId.equals(w.getSession().getId()) && jobId.equals(w.getJobId());
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobWorker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobWorker.java
index fa8d670..09ef5ad 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobWorker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/job/GridJobWorker.java
@@ -80,6 +80,12 @@
 import static org.apache.ignite.internal.GridTopic.TOPIC_TASK;
 import static org.apache.ignite.internal.managers.communication.GridIoPolicy.MANAGEMENT_POOL;
 import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.CANCELLED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FAILED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FINISHED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.QUEUED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.RUNNING;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.SUSPENDED;
 
 /**
  * Job worker.
@@ -179,6 +185,9 @@
     /** Security context. */
     private final SecurityContext secCtx;
 
+    /** Job status. */
+    private volatile ComputeJobStatusEnum status = QUEUED;
+
     /**
      * @param ctx Kernal context.
      * @param dep Grid deployment.
@@ -442,9 +451,12 @@
 
         boolean res;
 
-        if (res = holdLsnr.onHeld(this))
+        if (res = holdLsnr.onHeld(this)) {
             held.incrementAndGet();
 
+            status = SUSPENDED;
+        }
+
         return res;
     }
 
@@ -478,6 +490,9 @@
             // Inject resources.
             ctx.resource().inject(dep, taskCls, job, ses, jobCtx);
 
+            // Event notification.
+            evtLsnr.onJobQueued(this);
+
             if (!internal && ctx.event().isRecordable(EVT_JOB_QUEUED))
                 recordEvent(EVT_JOB_QUEUED, "Job got queued for computation.");
 
@@ -513,6 +528,8 @@
 
         isStarted = true;
 
+        status = RUNNING;
+
         // Event notification.
         evtLsnr.onJobStarted(this);
 
@@ -565,8 +582,10 @@
                 super.cancel();
 
             if (!skipNtf) {
-                if (holdLsnr.onUnheld(this))
-                    held.decrementAndGet();
+                if (holdLsnr.onUnheld(this)) {
+                    if (held.decrementAndGet() == 0)
+                        status = RUNNING;
+                }
                 else {
                     if (log.isDebugEnabled())
                         log.debug("Ignoring job execution (job was not held).");
@@ -753,11 +772,11 @@
                 if (log.isDebugEnabled())
                     log.debug("Cancelling job: " + ses);
 
-                U.wrapThreadLoader(dep.classLoader(), new IgniteRunnable() {
-                    @Override public void run() {
-                        try (OperationSecurityContext c = ctx.security().withContext(secCtx)) {
-                            job0.cancel();
-                        }
+                status = CANCELLED;
+
+                U.wrapThreadLoader(dep.classLoader(), (IgniteRunnable)() -> {
+                    try (OperationSecurityContext c = ctx.security().withContext(secCtx)) {
+                        job0.cancel();
                     }
                 });
             }
@@ -814,9 +833,11 @@
      * @param ex Error.
      * @param sndReply If {@code true}, reply will be sent.
      */
-    void finishJob(@Nullable Object res,
+    void finishJob(
+        @Nullable Object res,
         @Nullable IgniteException ex,
-        boolean sndReply) {
+        boolean sndReply
+    ) {
         finishJob(res, ex, sndReply, false);
     }
 
@@ -826,11 +847,12 @@
      * @param sndReply If {@code true}, reply will be sent.
      * @param retry If {@code true}, retry response will be sent.
      */
-    void finishJob(@Nullable Object res,
+    void finishJob(
+        @Nullable Object res,
         @Nullable IgniteException ex,
         boolean sndReply,
-        boolean retry)
-    {
+        boolean retry
+    ) {
         // Avoid finishing a job more than once from different threads.
         if (!finishing.compareAndSet(false, true))
             return;
@@ -859,6 +881,8 @@
                         U.warn(log, "Failed to reply to sender node because it left grid [nodeId=" + taskNode.id() +
                             ", ses=" + ses + ", jobId=" + ses.getJobId() + ", job=" + job + ']');
 
+                        status = FAILED;
+
                         // Record job reply failure.
                         if (!internal && ctx.event().isRecordable(EVT_JOB_FAILED))
                             evts = addEvent(evts, EVT_JOB_FAILED, "Job reply failed (task node left grid): " + job);
@@ -873,7 +897,7 @@
 
                             Map<Object, Object> attrs = jobCtx.getAttributes();
 
-                            // Try serialize response, and if exception - return to client.
+                            // Try to serialize response, and if exception - return to client.
                             if (!loc) {
                                 try {
                                     resBytes = U.marshal(marsh, res);
@@ -924,6 +948,8 @@
                             }
 
                             if (ex != null) {
+                                status = FAILED;
+
                                 if (isStarted) {
                                     // Job failed.
                                     if (!internal && ctx.event().isRecordable(EVT_JOB_FAILED))
@@ -934,8 +960,12 @@
                                     evts = addEvent(evts, EVT_JOB_REJECTED, "Job has not been started " +
                                         "[ex=" + ex + ", job=" + job + ']');
                             }
-                            else if (!internal && ctx.event().isRecordable(EVT_JOB_FINISHED))
-                                evts = addEvent(evts, EVT_JOB_FINISHED, /*no message for success. */null);
+                            else {
+                                status = FINISHED;
+
+                                if (!internal && ctx.event().isRecordable(EVT_JOB_FINISHED))
+                                    evts = addEvent(evts, EVT_JOB_FINISHED, /*no message for success. */null);
+                            }
 
                             GridJobExecuteResponse jobRes = new GridJobExecuteResponse(
                                 ctx.localNodeId(),
@@ -1006,6 +1036,8 @@
                 }
                 else {
                     if (ex != null) {
+                        status = FAILED;
+
                         if (isStarted) {
                             if (!internal && ctx.event().isRecordable(EVT_JOB_FAILED))
                                 evts = addEvent(evts, EVT_JOB_FAILED, "Job failed due to exception [ex=" + ex +
@@ -1015,13 +1047,21 @@
                             evts = addEvent(evts, EVT_JOB_REJECTED, "Job has not been started [ex=" + ex +
                                 ", job=" + job + ']');
                     }
-                    else if (!internal && ctx.event().isRecordable(EVT_JOB_FINISHED))
-                        evts = addEvent(evts, EVT_JOB_FINISHED, /*no message for success. */null);
+                    else {
+                        status = FINISHED;
+
+                        if (!internal && ctx.event().isRecordable(EVT_JOB_FINISHED))
+                            evts = addEvent(evts, EVT_JOB_FINISHED, /*no message for success. */null);
+                    }
                 }
             }
-            // Job timed out.
-            else if (!internal && ctx.event().isRecordable(EVT_JOB_FAILED))
-                evts = addEvent(evts, EVT_JOB_FAILED, "Job failed due to timeout: " + job);
+            else {
+                // Job timed out.
+                status = FAILED;
+
+                if (!internal && ctx.event().isRecordable(EVT_JOB_FAILED))
+                    evts = addEvent(evts, EVT_JOB_FAILED, "Job failed due to timeout: " + job);
+            }
         }
         finally {
             if (evts != null) {
@@ -1103,6 +1143,13 @@
         return ctx.discovery().node(uid) == null || !ctx.discovery().pingNodeNoError(uid);
     }
 
+    /**
+     * @return Job status.
+     */
+    ComputeJobStatusEnum status() {
+        return status;
+    }
+
     /** {@inheritDoc} */
     @Override public int hashCode() {
         IgniteUuid jobId = ses.getJobId();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
index 40f66d6..25b4b40 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
@@ -223,7 +223,10 @@
         catch (Throwable e) {
             handler.unregisterRequest(req.requestId());
 
-            U.error(log, "Failed to process client request [req=" + req + ", msg=" + e.getMessage() + "]", e);
+            if (e instanceof Error)
+                U.error(log, "Failed to process client request [req=" + req + ", msg=" + e.getMessage() + "]", e);
+            else
+                U.warn(log, "Failed to process client request [req=" + req + ", msg=" + e.getMessage() + "]", e);
 
             ses.send(parser.encode(handler.handleException(e, req)));
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/PlatformContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/PlatformContextImpl.java
index d8c8d21..f4512f7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/PlatformContextImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/PlatformContextImpl.java
@@ -607,7 +607,7 @@
 
         Boolean useTls = platformCacheUpdateUseThreadLocal.get();
         if (useTls != null && useTls) {
-            long cacheIdAndPartition = ((long)part << 32) + cacheId;
+            long cacheIdAndPartition = ((long)part << 32) | cacheId;
 
             gateway().platformCacheUpdateFromThreadLocal(
                     cacheIdAndPartition, ver.topologyVersion(), ver.minorTopologyVersion());
@@ -681,7 +681,7 @@
     }
 
     /** {@inheritDoc} */
-    @Override public void onDoneAfterTopologyUnlock(GridDhtPartitionsExchangeFuture fut) {
+    @Override public void onDoneBeforeTopologyUnlock(GridDhtPartitionsExchangeFuture fut) {
         AffinityTopologyVersion ver = fut.topologyVersion();
 
         if (ver != null) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/cache/PlatformCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/cache/PlatformCache.java
index c8326f8..374cb1e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/cache/PlatformCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/cache/PlatformCache.java
@@ -377,6 +377,9 @@
     /** */
     public static final int OP_INVOKE_JAVA = 98;
 
+    /** */
+    public static final int OP_PERSISTENCE_ENABLED = 99;
+
     /** Underlying JCache in binary mode. */
     private final IgniteCacheProxy cache;
 
@@ -1265,6 +1268,9 @@
 
                 return FALSE;
             }
+
+            case OP_PERSISTENCE_ENABLED:
+                return cache.context().group().persistenceEnabled() ? TRUE : FALSE;
         }
         return super.processInLongOutLong(type, val);
     }
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 c9ce397..ab8ac57 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
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.platform.client;
 
 import java.util.EnumSet;
+import org.apache.ignite.client.ClientServices;
 import org.apache.ignite.internal.ThinProtocolFeature;
 
 /**
@@ -49,7 +50,13 @@
     QRY_PARTITIONS_BATCH_SIZE(7),
 
     /** Binary configuration retrieval. */
-    BINARY_CONFIGURATION(8);
+    BINARY_CONFIGURATION(8),
+
+    /** Handle of {@link ClientServices#serviceDescriptors()}. */
+    GET_SERVICE_DESCRIPTORS(9),
+
+    /** Invoke service methods with caller context. */
+    SERVICE_INVOKE_CALLCTX(10);
 
     /** */
     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 daa2b83..f3fefeb 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
@@ -78,6 +78,8 @@
 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.ClientServiceGetDescriptorRequest;
+import org.apache.ignite.internal.processors.platform.client.service.ClientServiceGetDescriptorsRequest;
 import org.apache.ignite.internal.processors.platform.client.service.ClientServiceInvokeRequest;
 import org.apache.ignite.internal.processors.platform.client.streamer.ClientDataStreamerAddDataRequest;
 import org.apache.ignite.internal.processors.platform.client.streamer.ClientDataStreamerStartRequest;
@@ -268,6 +270,12 @@
     /** Service invocation. */
     private static final short OP_SERVICE_INVOKE = 7000;
 
+    /** Get service descriptors. */
+    private static final short OP_SERVICE_GET_DESCRIPTORS = 7001;
+
+    /** Get service descriptor. */
+    private static final short OP_SERVICE_GET_DESCRIPTOR = 7002;
+
     /** Data streamers. */
     /** */
     private static final short OP_DATA_STREAMER_START = 8000;
@@ -483,7 +491,13 @@
                 return new ClientExecuteTaskRequest(reader);
 
             case OP_SERVICE_INVOKE:
-                return new ClientServiceInvokeRequest(reader);
+                return new ClientServiceInvokeRequest(reader, protocolCtx);
+
+            case OP_SERVICE_GET_DESCRIPTORS:
+                return new ClientServiceGetDescriptorsRequest(reader);
+
+            case OP_SERVICE_GET_DESCRIPTOR:
+                return new ClientServiceGetDescriptorRequest(reader);
 
             case OP_DATA_STREAMER_START:
                 return new ClientDataStreamerStartRequest(reader);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorResponse.java
new file mode 100644
index 0000000..79573ca
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorResponse.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.apache.ignite.internal.binary.BinaryRawWriterEx;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+import org.apache.ignite.services.ServiceDescriptor;
+
+import static org.apache.ignite.internal.processors.platform.client.service.ClientServiceDescriptorsResponse.writeDescriptor;
+
+/** Service descriptor. */
+public class ClientServiceDescriptorResponse extends ClientResponse {
+    /** Services descriptor. */
+    private final ServiceDescriptor svc;
+
+    /**
+     * @param reqId Request id.
+     * @param svc Services descriptor.
+     */
+    public ClientServiceDescriptorResponse(long reqId, ServiceDescriptor svc) {
+        super(reqId);
+
+        this.svc = svc;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void encode(ClientConnectionContext ctx, BinaryRawWriterEx writer) {
+        super.encode(ctx, writer);
+
+        writeDescriptor(writer, svc);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorsResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorsResponse.java
new file mode 100644
index 0000000..f5ba7d9
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceDescriptorsResponse.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util.Collection;
+import org.apache.ignite.internal.binary.BinaryRawWriterEx;
+import org.apache.ignite.internal.client.thin.ClientUtils;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+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.services.ServiceDescriptor;
+
+/** Service descriptors. */
+public class ClientServiceDescriptorsResponse extends ClientResponse {
+    /** Services descriptors. */
+    private final Collection<ServiceDescriptor> svcs;
+
+    /**
+     * @param reqId Request id.
+     * @param svcs Services descriptors.
+     */
+    public ClientServiceDescriptorsResponse(long reqId, Collection<ServiceDescriptor> svcs) {
+        super(reqId);
+
+        this.svcs = svcs;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void encode(ClientConnectionContext ctx, BinaryRawWriterEx writer) {
+        super.encode(ctx, writer);
+
+        ClientUtils.collection(svcs, writer.out(), (out, svc) -> writeDescriptor(writer, svc));
+    }
+
+    /** */
+    public static void writeDescriptor(BinaryRawWriterEx writer, ServiceDescriptor svc) {
+        writer.writeString(svc.name());
+        writer.writeString(svc.serviceClass().getName());
+        writer.writeInt(svc.totalCount());
+        writer.writeInt(svc.maxPerNodeCount());
+        writer.writeString(svc.cacheName());
+        writer.writeUuid(svc.originNodeId());
+        writer.writeByte(PlatformService.class.isAssignableFrom(svc.serviceClass())
+            ? PlatformServices.PLATFORM_DOTNET
+            : PlatformServices.PLATFORM_JAVA);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorRequest.java
new file mode 100644
index 0000000..d87d2ab
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.apache.ignite.binary.BinaryRawReader;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientRequest;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+
+import static org.apache.ignite.internal.processors.platform.client.service.ClientServiceInvokeRequest.findServiceDescriptor;
+
+/**
+ * Request to get service descriptor.
+ */
+public class ClientServiceGetDescriptorRequest extends ClientRequest {
+    /** Service name. */
+    private final String name;
+
+    /**
+     * Constructor.
+     *
+     * @param reader Reader.
+     */
+    public ClientServiceGetDescriptorRequest(BinaryRawReader reader) {
+        super(reader);
+
+        name = reader.readString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientResponse process(ClientConnectionContext ctx) {
+        return new ClientServiceDescriptorResponse(requestId(), findServiceDescriptor(ctx, name));
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorsRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorsRequest.java
new file mode 100644
index 0000000..c25a136
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceGetDescriptorsRequest.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.apache.ignite.binary.BinaryRawReader;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientRequest;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+
+/**
+ * Request to get service descriptors.
+ */
+public class ClientServiceGetDescriptorsRequest extends ClientRequest {
+    /**
+     * Constructor.
+     *
+     * @param reader Reader.
+     */
+    public ClientServiceGetDescriptorsRequest(BinaryRawReader reader) {
+        super(reader);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientResponse process(ClientConnectionContext ctx) {
+        return new ClientServiceDescriptorsResponse(requestId(), ctx.kernalContext().service().serviceDescriptors());
+    }
+}
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
index 7cace9c..907f95a 100644
--- 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
@@ -27,12 +27,15 @@
 import org.apache.ignite.IgniteBinary;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteServices;
+import org.apache.ignite.internal.IgniteServicesImpl;
+import org.apache.ignite.internal.binary.BinaryArray;
 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.ClientProtocolContext;
 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;
@@ -43,6 +46,8 @@
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceDescriptor;
 
+import static org.apache.ignite.internal.processors.platform.client.ClientBitmaskFeature.SERVICE_INVOKE_CALLCTX;
+
 /**
  * Request to invoke service method.
  */
@@ -80,12 +85,16 @@
     /** Objects reader. */
     private final BinaryRawReaderEx reader;
 
+    /** Service call context attributes. */
+    private final Map<String, Object> callAttrs;
+
     /**
      * Constructor.
      *
      * @param reader Reader.
+     * @param protocolCtx Protocol context.
      */
-    public ClientServiceInvokeRequest(BinaryReaderExImpl reader) {
+    public ClientServiceInvokeRequest(BinaryReaderExImpl reader, ClientProtocolContext protocolCtx) {
         super(reader);
 
         name = reader.readString();
@@ -126,6 +135,8 @@
             args[i] = reader.readObjectDetached();
         }
 
+        callAttrs = protocolCtx.isFeatureSupported(SERVICE_INVOKE_CALLCTX) ? reader.readMap() : null;
+
         reader.in().position(argsStartPos);
     }
 
@@ -152,9 +163,10 @@
 
             if (PlatformService.class.isAssignableFrom(svcCls)) {
                 // Never deserialize platform service arguments and result: may contain platform-only types.
-                PlatformService proxy = services.serviceProxy(name, PlatformService.class, false, timeout);
+                PlatformService proxy =
+                    ((IgniteServicesImpl)services).serviceProxy(name, PlatformService.class, false, timeout, true);
 
-                res = proxy.invokeMethod(methodName, keepBinary(), false, args, null);
+                res = proxy.invokeMethod(methodName, keepBinary(), false, args, callAttrs);
             }
             else {
                 // Deserialize Java service arguments when not in keepBinary mode.
@@ -168,13 +180,14 @@
                 }
 
                 GridServiceProxy<?> proxy = new GridServiceProxy<>(grp, name, Service.class, false, timeout,
-                    ctx.kernalContext(), null);
+                    ctx.kernalContext(), null, true);
 
                 Method method = resolveMethod(ctx, svcCls);
 
-                PlatformServices.convertArrayArgs(args, method);
+                if (!BinaryArray.useBinaryArrays())
+                    PlatformServices.convertArrayArgs(args, method);
 
-                res = proxy.invokeMethod(method, args, null);
+                res = proxy.invokeMethod(method, args, callAttrs);
             }
 
             return new ClientObjectResponse(requestId(), res);
@@ -207,7 +220,7 @@
      * @param ctx Connection context.
      * @param name Service name.
      */
-    private static ServiceDescriptor findServiceDescriptor(ClientConnectionContext ctx, String name) {
+    public static ServiceDescriptor findServiceDescriptor(ClientConnectionContext ctx, String name) {
         for (ServiceDescriptor desc : ctx.kernalContext().service().serviceDescriptors()) {
             if (name.equals(desc.name()))
                 return desc;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/compute/PlatformCompute.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/compute/PlatformCompute.java
index aa73c0a..982f85f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/compute/PlatformCompute.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/compute/PlatformCompute.java
@@ -30,6 +30,7 @@
 import org.apache.ignite.compute.ComputeTaskFuture;
 import org.apache.ignite.internal.IgniteComputeImpl;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryObjectImpl;
 import org.apache.ignite.internal.binary.BinaryRawReaderEx;
 import org.apache.ignite.internal.binary.BinaryRawWriterEx;
@@ -363,7 +364,7 @@
 
         IgniteCompute compute0 = computeForTask(nodeIds);
 
-        if (!keepBinary && arg instanceof BinaryObjectImpl)
+        if (!keepBinary && (arg instanceof BinaryObjectImpl || arg instanceof BinaryArray))
             arg = ((BinaryObject)arg).deserialize();
 
         if (async)
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 e32342d..9178acc 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
@@ -28,6 +28,8 @@
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteServices;
+import org.apache.ignite.internal.IgniteServicesImpl;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryRawReaderEx;
 import org.apache.ignite.internal.binary.BinaryRawWriterEx;
 import org.apache.ignite.internal.processors.platform.PlatformAbstractTarget;
@@ -41,7 +43,6 @@
 import org.apache.ignite.internal.processors.platform.utils.PlatformWriterBiClosure;
 import org.apache.ignite.internal.processors.platform.utils.PlatformWriterClosure;
 import org.apache.ignite.internal.processors.service.GridServiceProxy;
-import org.apache.ignite.internal.processors.service.ServiceCallContextImpl;
 import org.apache.ignite.internal.util.typedef.T3;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.lang.IgnitePredicate;
@@ -51,6 +52,8 @@
 import org.apache.ignite.services.ServiceDescriptor;
 import org.jetbrains.annotations.NotNull;
 
+import static org.apache.ignite.internal.IgniteServicesImpl.DFLT_TIMEOUT;
+
 /**
  * Interop services.
  */
@@ -104,10 +107,10 @@
     private static final int OP_DOTNET_DEPLOY_ALL_ASYNC = 16;
 
     /** */
-    private static final byte PLATFORM_JAVA = 0;
+    public static final byte PLATFORM_JAVA = 0;
 
     /** */
-    private static final byte PLATFORM_DOTNET = 1;
+    public static final byte PLATFORM_DOTNET = 1;
 
     /** */
     private static final CopyOnWriteConcurrentMap<T3<Class, String, Integer>, Method> SVC_METHODS
@@ -384,9 +387,9 @@
                     throw new IgniteException("Failed to find deployed service: " + name);
 
                 Object proxy = PlatformService.class.isAssignableFrom(d.serviceClass())
-                    ? services.serviceProxy(name, PlatformService.class, sticky)
+                    ? ((IgniteServicesImpl)services).serviceProxy(name, PlatformService.class, sticky, DFLT_TIMEOUT, true)
                     : new GridServiceProxy<>(services.clusterGroup(), name, Service.class, sticky, 0,
-                        platformCtx.kernalContext(), null);
+                        platformCtx.kernalContext(), null, true);
 
                 return new ServiceProxyHolder(proxy, d.serviceClass(), platformContext());
             }
@@ -624,10 +627,11 @@
                     args = PlatformUtils.unwrapBinariesInArray(args);
 
                 Method mtd = getMethod(serviceClass, mthdName, args);
-                convertArrayArgs(args, mtd);
 
-                return ((GridServiceProxy)proxy)
-                    .invokeMethod(mtd, args, callAttrs == null ? null : new ServiceCallContextImpl(callAttrs));
+                if (!BinaryArray.useBinaryArrays())
+                    convertArrayArgs(args, mtd);
+
+                return ((GridServiceProxy)proxy).invokeMethod(mtd, args, callAttrs);
             }
         }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
index 87d8614..9987897 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
@@ -72,6 +72,7 @@
 import org.apache.ignite.configuration.PersistentStoreConfiguration;
 import org.apache.ignite.configuration.PlatformCacheConfiguration;
 import org.apache.ignite.configuration.SqlConnectorConfiguration;
+import org.apache.ignite.configuration.SystemDataRegionConfiguration;
 import org.apache.ignite.configuration.ThinClientConfiguration;
 import org.apache.ignite.configuration.TransactionConfiguration;
 import org.apache.ignite.configuration.WALMode;
@@ -2088,6 +2089,9 @@
         if (in.readBoolean())
             res.setDefaultDataRegionConfiguration(readDataRegionConfiguration(in));
 
+        if (in.readBoolean())
+            res.setSystemDataRegionConfiguration(readSystemDataRegionConfiguration(in));
+
         return res;
     }
 
@@ -2233,6 +2237,13 @@
             }
             else
                 w.writeBoolean(false);
+
+            if (cfg.getSystemDataRegionConfiguration() != null) {
+                w.writeBoolean(true);
+                writeSystemDataRegionConfiguration(w, cfg.getSystemDataRegionConfiguration());
+            }
+            else
+                w.writeBoolean(false);
         }
         else
             w.writeBoolean(false);
@@ -2263,6 +2274,20 @@
     }
 
     /**
+     * Writes the system data region configuration.
+     *
+     * @param w Writer.
+     * @param cfg System data region configuration.
+     */
+    private static void writeSystemDataRegionConfiguration(BinaryRawWriter w, SystemDataRegionConfiguration cfg) {
+        assert w != null;
+        assert cfg != null;
+
+        w.writeLong(cfg.getInitialSize());
+        w.writeLong(cfg.getMaxSize());
+    }
+
+    /**
      * Writes the SSL context factory.
      *
      * @param w Writer.
@@ -2321,6 +2346,19 @@
     }
 
     /**
+     * Reads the system data region configuration.
+     *
+     * @param r Reader.
+     */
+    private static SystemDataRegionConfiguration readSystemDataRegionConfiguration(BinaryRawReader r) {
+        assert r != null;
+
+        return new SystemDataRegionConfiguration()
+                .setInitialSize(r.readLong())
+                .setMaxSize(r.readLong());
+    }
+
+    /**
      * Reads the plugin configuration.
      *
      * @param cfg Ignite configuration to update.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformUtils.java
index 542b67d..4aa7649 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformUtils.java
@@ -1063,12 +1063,17 @@
                     throw new IgniteException("Java object/factory class field is not found [" +
                         "className=" + clsName + ", fieldName=" + fieldName + ']');
 
+                Object val = prop.getValue();
+
                 try {
-                    field.set(obj, prop.getValue());
+                    field.set(
+                        obj,
+                        BinaryUtils.isObjectArray(field.getType()) ? BinaryUtils.rawArrayFromBinary(val) : val
+                    );
                 }
                 catch (Exception e) {
                     throw new IgniteException("Failed to set Java object/factory field [className=" + clsName +
-                        ", fieldName=" + fieldName + ", fieldValue=" + prop.getValue() + ']', e);
+                        ", fieldName=" + fieldName + ", fieldValue=" + val + ']', e);
                 }
             }
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/pool/PoolProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/pool/PoolProcessor.java
index 92fb961..ec15118 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/pool/PoolProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/pool/PoolProcessor.java
@@ -56,6 +56,7 @@
 import org.apache.ignite.internal.util.StripedExecutor;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.internal.util.worker.GridWorkerListener;
 import org.apache.ignite.internal.worker.WorkersRegistry;
@@ -71,6 +72,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static org.apache.ignite.configuration.IgniteConfiguration.DFLT_THREAD_KEEP_ALIVE_TIME;
 import static org.apache.ignite.failure.FailureType.SYSTEM_WORKER_TERMINATION;
+import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.SNAPSHOT_RUNNER_THREAD_PREFIX;
 import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
 
 /**
@@ -198,6 +200,10 @@
     @GridToStringExclude
     private ThreadPoolExecutor rebalanceExecSvc;
 
+    /** Snapshot task executor service. */
+    @GridToStringExclude
+    private ThreadPoolExecutor snpExecSvc;
+
     /** Executor service for thin clients. */
     @GridToStringExclude
     private ExecutorService thinClientExec;
@@ -206,6 +212,10 @@
     @GridToStringExclude
     private IgniteStripedThreadPoolExecutor rebalanceStripedExecSvc;
 
+    /** Executor to perform a data pages scanning during cache group re-encryption. */
+    @GridToStringExclude
+    private ThreadPoolExecutor reencryptExecSvc;
+
     /** Map of {@link IoPool}-s injected by Ignite plugins. */
     private final IoPool[] extPools = new IoPool[128];
 
@@ -502,6 +512,32 @@
 
         rebalanceExecSvc.allowCoreThreadTimeOut(true);
 
+        if (CU.isPersistenceEnabled(ctx.config())) {
+            snpExecSvc = createExecutorService(
+                SNAPSHOT_RUNNER_THREAD_PREFIX,
+                cfg.getIgniteInstanceName(),
+                cfg.getSnapshotThreadPoolSize(),
+                cfg.getSnapshotThreadPoolSize(),
+                DFLT_THREAD_KEEP_ALIVE_TIME,
+                new LinkedBlockingQueue<>(),
+                GridIoPolicy.UNDEFINED,
+                excHnd);
+
+            snpExecSvc.allowCoreThreadTimeOut(true);
+
+            reencryptExecSvc = createExecutorService(
+                "reencrypt",
+                ctx.igniteInstanceName(),
+                1,
+                1,
+                DFLT_THREAD_KEEP_ALIVE_TIME,
+                new LinkedBlockingQueue<>(),
+                GridIoPolicy.UNDEFINED,
+                oomeHnd);
+
+            reencryptExecSvc.allowCoreThreadTimeOut(true);
+        }
+
         if (cfg.getClientConnectorConfiguration() != null) {
             thinClientExec = new IgniteThreadPoolExecutor(
                 "client-connector",
@@ -579,9 +615,15 @@
             monitorStripedPool("StripedExecutor", stripedExecSvc);
         }
 
+        if (snpExecSvc != null)
+            monitorExecutor("GridSnapshotExecutor", snpExecSvc);
+
         if (thinClientExec != null)
             monitorExecutor("GridThinClientExecutor", thinClientExec);
 
+        if (reencryptExecSvc != null)
+            monitorExecutor("GridReencryptionExecutor", reencryptExecSvc);
+
         if (customExecs != null) {
             for (Map.Entry<String, ? extends ExecutorService> entry : customExecs.entrySet())
                 monitorExecutor(entry.getKey(), entry.getValue());
@@ -839,6 +881,13 @@
     }
 
     /**
+     * @return Executor service that is used for processing snapshot tasks (taking, sending, restoring).
+     */
+    public ExecutorService getSnapshotExecutorService() {
+        return snpExecSvc;
+    }
+
+    /**
      * Executor service for thin clients.
      *
      * @return Executor service for thin clients.
@@ -866,6 +915,13 @@
     }
 
     /**
+     * @return Executor to perform a data pages scanning during cache group re-encryption.
+     */
+    public ExecutorService getReencryptionExecutorService() {
+        return reencryptExecSvc;
+    }
+
+    /**
      * Creates a {@link MetricRegistry} for an executor.
      *
      * @param name Name of the metric to register.
@@ -1004,6 +1060,9 @@
             registerStripedExecutorMBean(mbMgr, "StripedExecutor", stripedExecSvc);
         }
 
+        if (snpExecSvc != null)
+            registerExecutorMBean(mbMgr, "GridSnapshotExecutor", snpExecSvc);
+
         if (thinClientExec != null)
             registerExecutorMBean(mbMgr, "GridThinClientExecutor", thinClientExec);
 
@@ -1059,6 +1118,9 @@
      */
     private void stopExecutors0(IgniteLogger log) {
         assert log != null;
+        U.shutdownNow(getClass(), snpExecSvc, log);
+
+        snpExecSvc = null;
 
         U.shutdownNow(getClass(), execSvc, log);
 
@@ -1134,6 +1196,10 @@
 
         thinClientExec = null;
 
+        U.shutdownNow(getClass(), reencryptExecSvc, log);
+
+        reencryptExecSvc = null;
+
         if (!F.isEmpty(customExecs)) {
             for (ThreadPoolExecutor exec : customExecs.values())
                 U.shutdownNow(getClass(), exec, log);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
index 461a559..392a8c2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
@@ -64,7 +64,6 @@
 import org.apache.ignite.internal.cache.query.index.IndexProcessor;
 import org.apache.ignite.internal.cache.query.index.IndexQueryProcessor;
 import org.apache.ignite.internal.cache.query.index.IndexQueryResult;
-import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.managers.communication.GridMessageListener;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
@@ -3369,8 +3368,8 @@
      * @param cacheName Cache name.
      * @param valCls Cache value class.
      * @param idxQryDesc Index query description.
-     * @param filter Optional user defined cache entries filter.
-     * @param filters Ignite specific cache entries filters.
+     * @param entryFilter Optional user defined cache entries filter.
+     * @param cacheFilter Ignite specific cache entries filters.
      * @param keepBinary Keep binary flag.
      * @return Key/value rows.
      * @throws IgniteCheckedException If failed.
@@ -3379,8 +3378,8 @@
         String cacheName,
         String valCls,
         final IndexQueryDesc idxQryDesc,
-        @Nullable IgniteBiPredicate<K, V> filter,
-        final IndexingQueryFilter filters,
+        @Nullable IgniteBiPredicate<K, V> entryFilter,
+        final IndexingQueryFilter cacheFilter,
         boolean keepBinary
     ) throws IgniteCheckedException {
         if (!busyLock.enterBusy())
@@ -3392,10 +3391,7 @@
             return executeQuery(GridCacheQueryType.INDEX, valCls, cctx,
                 new IgniteOutClosureX<IndexQueryResult<K, V>>() {
                     @Override public IndexQueryResult<K, V> applyx() throws IgniteCheckedException {
-                        IndexQueryContext qryCtx = new IndexQueryContext(filters, null);
-
-                        return idxQryPrc.queryLocal(cctx, idxQryDesc, filter, qryCtx, keepBinary);
-
+                        return idxQryPrc.queryLocal(cctx, idxQryDesc, entryFilter, cacheFilter, keepBinary);
                     }
                 }, true);
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
index d83744d..308e736 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
@@ -31,6 +31,8 @@
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.cache.QueryIndexType;
+import org.apache.ignite.internal.binary.BinaryArray;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.cache.KeyCacheObject;
@@ -634,11 +636,11 @@
             }
 
             if (validateTypes && propVal != null) {
-                if (!(propVal instanceof BinaryObject)) {
+                if (!(propVal instanceof BinaryObject) || propVal instanceof BinaryArray) {
                     if (!U.box(prop.type()).isAssignableFrom(U.box(propVal.getClass()))) {
                         // Some reference type arrays end up being converted to Object[]
-                        if (!(prop.type().isArray() && Object[].class == propVal.getClass() &&
-                            Arrays.stream((Object[])propVal).
+                        if (!(prop.type().isArray() && BinaryUtils.isObjectArray(propVal.getClass()) &&
+                            Arrays.stream(BinaryUtils.rawArrayFromBinary(propVal)).
                             noneMatch(x -> x != null && !U.box(prop.type().getComponentType()).isAssignableFrom(U.box(x.getClass())))))
                         {
                             throw new IgniteSQLException("Type for a column '" + prop.name() +
@@ -719,11 +721,11 @@
                 if (propVal == null)
                     continue;
 
-                if (!(propVal instanceof BinaryObject)) {
+                if (!(propVal instanceof BinaryObject) || propVal instanceof BinaryArray) {
                     if (!U.box(propType).isAssignableFrom(U.box(propVal.getClass()))) {
                         // Some reference type arrays end up being converted to Object[]
-                        if (!(propType.isArray() && Object[].class == propVal.getClass() &&
-                            Arrays.stream((Object[])propVal).
+                        if (!(propType.isArray() && BinaryUtils.isObjectArray(propVal.getClass()) &&
+                            Arrays.stream(BinaryUtils.rawArrayFromBinary(propVal)).
                                 noneMatch(x -> x != null && !U.box(propType.getComponentType()).isAssignableFrom(U.box(x.getClass())))))
                         {
                             throw new IgniteSQLException("Type for a column '" + idxField +
@@ -732,20 +734,13 @@
                                 propVal.getClass().getSimpleName() + "'");
                         }
                     }
-                }
-                else if (coCtx.kernalContext().cacheObjects().typeId(propType.getName()) !=
+                } else if (coCtx.kernalContext().cacheObjects().typeId(propType.getName()) !=
                     ((BinaryObject)propVal).type().typeId()) {
                     // Check for classes/enums implementing indexed interfaces.
-                    String clsName = ((BinaryObject)propVal).type().typeName();
-                    try {
-                        final Class<?> cls = Class.forName(clsName);
+                    final Class<?> cls = U.classForName(((BinaryObject)propVal).type().typeName(), null, true);
 
-                        if (propType.isAssignableFrom(cls))
-                            continue;
-                    } catch (ClassNotFoundException e) {
-                        if (log.isDebugEnabled())
-                            U.error(log, "Failed to find child class: " + clsName, e);
-                    }
+                    if ((cls == null && propType == Object.class) || (cls != null && propType.isAssignableFrom(cls)))
+                        continue;
 
                     throw new IgniteSQLException("Type for a column '" + idxField +
                         "' is not compatible with index definition. Expected '" +
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/RunningQueryManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/RunningQueryManager.java
index d6ae245..8308e1d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/RunningQueryManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/RunningQueryManager.java
@@ -67,6 +67,9 @@
     /** */
     public static final String SQL_QRY_HIST_VIEW_DESC = "SQL queries history.";
 
+    /** Undefined query ID value. */
+    public static final long UNDEFINED_QUERY_ID = 0L;
+
     /** Keep registered user queries. */
     private final ConcurrentMap<Long, GridRunningQueryInfo> runs = new ConcurrentHashMap<>();
 
@@ -137,16 +140,16 @@
     }
 
     /**
-     * Register running query.
+     * Registers running query and returns an id associated with the query.
      *
      * @param qry Query text.
      * @param qryType Query type.
      * @param schemaName Schema name.
      * @param loc Local query flag.
      * @param cancel Query cancel. Should be passed in case query is cancelable, or {@code null} otherwise.
-     * @return Id of registered query.
+     * @return Id of registered query. Id is a positive number.
      */
-    public Long register(String qry, GridCacheQueryType qryType, String schemaName, boolean loc,
+    public long register(String qry, GridCacheQueryType qryType, String schemaName, boolean loc,
         @Nullable GridQueryCancel cancel,
         String qryInitiatorId) {
         long qryId = qryIdGen.incrementAndGet();
@@ -185,8 +188,8 @@
      * @param qryId id of the query, which is given by {@link #register register} method.
      * @param failReason exception that caused query execution fail, or {@code null} if query succeded.
      */
-    public void unregister(Long qryId, @Nullable Throwable failReason) {
-        if (qryId == null)
+    public void unregister(long qryId, @Nullable Throwable failReason) {
+        if (qryId <= 0)
             return;
 
         boolean failed = failReason != null;
@@ -297,7 +300,7 @@
      *
      * @param qryId Query id.
      */
-    public void cancel(Long qryId) {
+    public void cancel(long qryId) {
         GridRunningQueryInfo run = runs.get(qryId);
 
         if (run != null)
@@ -336,10 +339,11 @@
 
     /**
      * Gets info about running query by their id.
-     * @param qryId
+     *
+     * @param qryId Query Id.
      * @return Running query info or {@code null} in case no running query for given id.
      */
-    public @Nullable GridRunningQueryInfo runningQueryInfo(Long qryId) {
+    public @Nullable GridRunningQueryInfo runningQueryInfo(long qryId) {
         return runs.get(qryId);
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/property/QueryBinaryProperty.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/property/QueryBinaryProperty.java
index e3c49e1..1a49d99 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/property/QueryBinaryProperty.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/property/QueryBinaryProperty.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.binary.BinaryObjectEx;
 import org.apache.ignite.internal.binary.BinaryObjectExImpl;
+import org.apache.ignite.internal.processors.cache.KeyCacheObjectImpl;
 import org.apache.ignite.internal.processors.query.GridQueryProperty;
 import org.apache.ignite.internal.util.typedef.F;
 
@@ -124,6 +125,11 @@
 
             return obj0.getField(propName);
         }
+        else if (obj instanceof KeyCacheObjectImpl) {
+            KeyCacheObjectImpl obj0 = (KeyCacheObjectImpl)obj;
+
+            return obj0.value(null, false);
+        }
         else
             throw new IgniteCheckedException("Unexpected binary object class [type=" + obj.getClass() + ']');
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceServiceInjector.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceServiceInjector.java
index a4b6cde..4853bf2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceServiceInjector.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceServiceInjector.java
@@ -23,6 +23,7 @@
 import org.apache.ignite.internal.IgniteServicesEx;
 import org.apache.ignite.internal.managers.deployment.GridDeployment;
 import org.apache.ignite.internal.processors.service.ServiceCallContextHolder;
+import org.apache.ignite.internal.processors.service.ServiceCallContextImpl;
 import org.apache.ignite.resources.ServiceResource;
 import org.apache.ignite.services.Service;
 import org.jetbrains.annotations.Nullable;
@@ -74,7 +75,11 @@
             ann.serviceName(),
             (Class<? super T>)ann.proxyInterface(),
             ann.proxySticky(),
-            ann.forwardCallerContext() ? ServiceCallContextHolder::current : null,
+            ann.forwardCallerContext() ? () -> {
+                ServiceCallContextImpl callCtx = ServiceCallContextHolder.current();
+
+                return callCtx == null ? null : callCtx.values();
+            } : null,
             0
         );
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignments.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignments.java
deleted file mode 100644
index 290fbf4..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignments.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.processors.service;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.Map;
-import java.util.UUID;
-import org.apache.ignite.cluster.ClusterNode;
-import org.apache.ignite.internal.processors.cache.GridCacheInternal;
-import org.apache.ignite.internal.util.tostring.GridToStringInclude;
-import org.apache.ignite.internal.util.typedef.internal.S;
-import org.apache.ignite.lang.IgnitePredicate;
-import org.apache.ignite.services.ServiceConfiguration;
-
-/**
- * Service per-node assignment.
- *
- * @deprecated Services internals use messages for deployment management instead of the utility cache, since Ignite 2.8.
- */
-@Deprecated
-public class GridServiceAssignments implements Serializable, GridCacheInternal {
-    /** Serialization version. */
-    private static final long serialVersionUID = 0L;
-
-    /** Node ID. */
-    private final UUID nodeId;
-
-    /** Topology version. */
-    private final long topVer;
-
-    /** Service configuration. */
-    private final ServiceConfiguration cfg;
-
-    /** Assignments. */
-    @GridToStringInclude
-    private Map<UUID, Integer> assigns = Collections.emptyMap();
-
-    /**
-     * @param cfg Configuration.
-     * @param nodeId Node ID.
-     * @param topVer Topology version.
-     */
-    public GridServiceAssignments(ServiceConfiguration cfg, UUID nodeId, long topVer) {
-        this.cfg = cfg;
-        this.nodeId = nodeId;
-        this.topVer = topVer;
-    }
-
-    /**
-     * @return Configuration.
-     */
-    public ServiceConfiguration configuration() {
-        return cfg;
-    }
-
-    /**
-     * @return Service name.
-     */
-    public String name() {
-        return cfg.getName();
-    }
-
-    /**
-     * @return Topology version.
-     */
-    public long topologyVersion() {
-        return topVer;
-    }
-
-    /**
-     * @return Cache name.
-     */
-    public String cacheName() {
-        return cfg.getCacheName();
-    }
-
-    /**
-     * @return Affinity key.
-     */
-    public Object affinityKey() {
-        return cfg.getAffinityKey();
-    }
-
-    /**
-     * @return Origin node ID.
-     */
-    public UUID nodeId() {
-        return nodeId;
-    }
-
-    /**
-     * @return Node filter.
-     */
-    public IgnitePredicate<ClusterNode> nodeFilter() {
-        return cfg.getNodeFilter();
-    }
-
-    /**
-     * @return Assignments.
-     */
-    public Map<UUID, Integer> assigns() {
-        return assigns;
-    }
-
-    /**
-     * @param assigns Assignments.
-     */
-    public void assigns(Map<UUID, Integer> assigns) {
-        this.assigns = assigns;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return S.toString(GridServiceAssignments.class, this);
-    }
-}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignmentsKey.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignmentsKey.java
deleted file mode 100644
index 593ae5c..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceAssignmentsKey.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.processors.service;
-
-import org.apache.ignite.internal.processors.cache.GridCacheUtilityKey;
-import org.apache.ignite.internal.util.typedef.internal.S;
-
-/**
- * Service configuration key.
- *
- * @deprecated Services internals use messages for deployment management instead of the utility cache, since Ignite 2.8.
- */
-@Deprecated
-public class GridServiceAssignmentsKey extends GridCacheUtilityKey<GridServiceAssignmentsKey> {
-    /** */
-    private static final long serialVersionUID = 0L;
-
-    /** Service name. */
-    private final String name;
-
-    /**
-     * @param name Service ID.
-     */
-    public GridServiceAssignmentsKey(String name) {
-        assert name != null;
-
-        this.name = name;
-    }
-
-    /**
-     * @return Service name.
-     */
-    public String name() {
-        return name;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected boolean equalsx(GridServiceAssignmentsKey that) {
-        return name.equals(that.name);
-    }
-
-    /** {@inheritDoc} */
-    @Override public int hashCode() {
-        return name.hashCode();
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return S.toString(GridServiceAssignmentsKey.class, this);
-    }
-}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeployment.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeployment.java
deleted file mode 100644
index 30f5724..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeployment.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.processors.service;
-
-import java.io.Serializable;
-import java.util.UUID;
-import org.apache.ignite.internal.processors.cache.GridCacheInternal;
-import org.apache.ignite.internal.util.typedef.internal.S;
-import org.apache.ignite.services.ServiceConfiguration;
-
-/**
- * Service deployment.
- *
- * @deprecated Services internals use messages for deployment management instead of the utility cache, since Ignite 2.8.
- */
-@Deprecated
-public class GridServiceDeployment implements GridCacheInternal, Serializable {
-    /** */
-    private static final long serialVersionUID = 0L;
-
-    /** Node ID. */
-    private UUID nodeId;
-
-    /** Service configuration. */
-    private ServiceConfiguration cfg;
-
-    /**
-     * @param nodeId Node ID.
-     * @param cfg Service configuration.
-     */
-    public GridServiceDeployment(UUID nodeId, ServiceConfiguration cfg) {
-        this.nodeId = nodeId;
-        this.cfg = cfg;
-    }
-
-    /**
-     * @return Node ID.
-     */
-    public UUID nodeId() {
-        return nodeId;
-    }
-
-    /**
-     * @return Service configuration.
-     */
-    public ServiceConfiguration configuration() {
-        return cfg;
-    }
-
-    /** {@inheritDoc} */
-    @Override public boolean equals(Object o) {
-        if (this == o)
-            return true;
-
-        if (o == null || getClass() != o.getClass())
-            return false;
-
-        GridServiceDeployment that = (GridServiceDeployment)o;
-
-        if (cfg != null ? !cfg.equals(that.cfg) : that.cfg != null)
-            return false;
-
-        if (nodeId != null ? !nodeId.equals(that.nodeId) : that.nodeId != null)
-            return false;
-
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override public int hashCode() {
-        int res = nodeId != null ? nodeId.hashCode() : 0;
-
-        res = 31 * res + (cfg != null ? cfg.hashCode() : 0);
-
-        return res;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return S.toString(GridServiceDeployment.class, this);
-    }
-}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeploymentKey.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeploymentKey.java
deleted file mode 100644
index b5f21d9..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceDeploymentKey.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.processors.service;
-
-import org.apache.ignite.internal.processors.cache.GridCacheUtilityKey;
-import org.apache.ignite.internal.util.typedef.internal.S;
-
-/**
- * Service configuration key.
- *
- * @deprecated Services internals use messages for deployment management instead of the utility cache, since Ignite 2.8.
- */
-@Deprecated
-public class GridServiceDeploymentKey extends GridCacheUtilityKey<GridServiceDeploymentKey> {
-    /** */
-    private static final long serialVersionUID = 0L;
-
-    /** Service name. */
-    private final String name;
-
-    /**
-     * @param name Service ID.
-     */
-    public GridServiceDeploymentKey(String name) {
-        assert name != null;
-
-        this.name = name;
-    }
-
-    /**
-     * @return Service name.
-     */
-    public String name() {
-        return name;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected boolean equalsx(GridServiceDeploymentKey that) {
-        return name.equals(that.name);
-    }
-
-    /** {@inheritDoc} */
-    @Override public int hashCode() {
-        return name.hashCode();
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return S.toString(GridServiceDeploymentKey.class, this);
-    }
-}
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 9c38aef..8f60b2a 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
@@ -32,6 +32,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
@@ -43,8 +44,11 @@
 import org.apache.ignite.internal.GridClosureCallMode;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryArray;
+import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.managers.communication.GridIoPolicy;
+import org.apache.ignite.internal.processors.metric.impl.HistogramMetricImpl;
 import org.apache.ignite.internal.processors.platform.PlatformNativeException;
 import org.apache.ignite.internal.processors.platform.services.PlatformService;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
@@ -53,10 +57,10 @@
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteCallable;
+import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.platform.PlatformServiceMethod;
 import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.services.Service;
-import org.apache.ignite.services.ServiceCallContext;
 import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.internal.processors.task.GridTaskThreadContextKey.TC_IO_POLICY;
@@ -110,6 +114,9 @@
     /** Service availability wait timeout. */
     private final long waitTimeout;
 
+    /** */
+    private final boolean keepBinary;
+
     /**
      * @param prj Grid projection.
      * @param name Service name.
@@ -117,7 +124,8 @@
      * @param sticky Whether multi-node request should be done.
      * @param timeout Service availability wait timeout. Cannot be negative.
      * @param ctx Context.
-     * @param callCtxProvider Caller context provider.
+     * @param callAttrsProvider Service call context attributes provider.
+     * @param keepBinary {@code True} if results should be in binary form.
      */
     public GridServiceProxy(ClusterGroup prj,
         String name,
@@ -125,7 +133,8 @@
         boolean sticky,
         long timeout,
         GridKernalContext ctx,
-        @Nullable Supplier<ServiceCallContext> callCtxProvider
+        @Nullable Supplier<Map<String, Object>> callAttrsProvider,
+        boolean keepBinary
     ) {
         assert timeout >= 0 : timeout;
 
@@ -133,6 +142,7 @@
         this.ctx = ctx;
         this.name = name;
         this.sticky = sticky;
+        this.keepBinary = keepBinary;
 
         waitTimeout = timeout;
         hasLocNode = hasLocalNode(prj);
@@ -142,7 +152,7 @@
         proxy = (T)Proxy.newProxyInstance(
             svc.getClassLoader(),
             new Class[] {svc},
-            new ProxyInvocationHandler(callCtxProvider)
+            new ProxyInvocationHandler(callAttrsProvider)
         );
     }
 
@@ -160,19 +170,15 @@
     }
 
     /**
-     * Invoek the method.
+     * Invoke the method.
      *
      * @param mtd Method.
      * @param args Arugments.
-     * @param callCtx Service call context.
+     * @param callAttrs Service call context attributes.
      * @return Result.
      */
     @SuppressWarnings("BusyWait")
-    public Object invokeMethod(
-        final Method mtd,
-        final Object[] args,
-        @Nullable ServiceCallContext callCtx
-    ) throws Throwable {
+    public Object invokeMethod(Method mtd, Object[] args, @Nullable Map<String, Object> callAttrs) throws Throwable {
         if (U.isHashCodeMethod(mtd))
             return System.identityHashCode(proxy);
         else if (U.isEqualsMethod(mtd))
@@ -201,21 +207,26 @@
                         if (svcCtx != null) {
                             Service svc = svcCtx.service();
 
-                            if (svc != null)
-                                return callServiceLocally(svc, mtd, args, callCtx);
+                            if (svc != null) {
+                                HistogramMetricImpl hist = svcCtx.isStatisticsEnabled() ?
+                                    svcCtx.metrics().findMetric(mtd.getName()) : null;
+
+                                return hist == null ? callServiceLocally(svc, mtd, args, callAttrs) :
+                                    measureCall(hist, () -> callServiceLocally(svc, mtd, args, callAttrs));
+                            }
                         }
                     }
                     else {
                         ctx.task().setThreadContext(TC_IO_POLICY, GridIoPolicy.SERVICE_POOL);
 
                         // Execute service remotely.
-                        return ctx.closure().callAsyncNoFailover(
+                        return unmarshalResult(ctx.closure().callAsyncNoFailover(
                             GridClosureCallMode.BROADCAST,
-                            new ServiceProxyCallable(methodName(mtd), name, mtd.getParameterTypes(), args, callCtx),
+                            new ServiceProxyCallable(methodName(mtd), name, mtd.getParameterTypes(), args, callAttrs),
                             Collections.singleton(node),
                             false,
                             waitTimeout,
-                            true).get();
+                            true).get());
                     }
                 }
                 catch (InvocationTargetException e) {
@@ -278,49 +289,58 @@
      * @param svc Service to be called.
      * @param mtd Method to call.
      * @param args Method args.
-     * @param callCtx Service call context.
+     * @param callAttrs Service call context attributes.
      * @return Invocation result.
      */
     private Object callServiceLocally(
         Service svc,
         Method mtd,
         Object[] args,
-        @Nullable ServiceCallContext callCtx
+        @Nullable Map<String, Object> callAttrs
     ) throws Exception {
-        if (svc instanceof PlatformService && !PLATFORM_SERVICE_INVOKE_METHOD.equals(mtd)) {
-            Map<String, Object> callAttrs = callCtx == null ? null : ((ServiceCallContextImpl)callCtx).values();
-
+        if (svc instanceof PlatformService && !PLATFORM_SERVICE_INVOKE_METHOD.equals(mtd))
             return ((PlatformService)svc).invokeMethod(methodName(mtd), false, true, args, callAttrs);
-        }
         else
-            return callServiceMethod(svc, mtd, args, callCtx);
+            return callServiceMethod(svc, mtd, args, callAttrs);
     }
 
     /**
      * @param svc Service to be called.
      * @param mtd Method to call.
      * @param args Method args.
-     * @param callCtx Service call context.
+     * @param callAttrs Service call context attributes.
      * @return Invocation result.
      */
     private static Object callServiceMethod(
         Service svc,
         Method mtd,
         Object[] args,
-        @Nullable ServiceCallContext callCtx
+        @Nullable Map<String, Object> callAttrs
     ) throws InvocationTargetException, IllegalAccessException {
-        if (callCtx != null)
-            ServiceCallContextHolder.current(callCtx);
+        if (callAttrs != null)
+            ServiceCallContextHolder.current(new ServiceCallContextImpl(callAttrs));
 
         try {
             return mtd.invoke(svc, args);
         }
         finally {
-            if (callCtx != null)
+            if (callAttrs != null)
                 ServiceCallContextHolder.current(null);
         }
     }
 
+    /** */
+    private Object unmarshalResult(byte[] res) throws IgniteCheckedException {
+        Marshaller marsh = ctx.config().getMarshaller();
+
+        if (keepBinary && BinaryArray.useBinaryArrays() && marsh instanceof BinaryMarshaller) {
+            // To avoid deserializing of enum types and BinaryArrays.
+            return ((BinaryMarshaller)marsh).binaryMarshaller().unmarshal(res, null);
+        }
+        else
+            return U.unmarshal(marsh, res, null);
+    }
+
     /**
      * @param sticky Whether multi-node request should be done.
      * @param name Service name.
@@ -434,36 +454,55 @@
     /**
      * @param mtd Method to invoke.
      */
-    String methodName(Method mtd) {
+    private static String methodName(Method mtd) {
         PlatformServiceMethod ann = mtd.getDeclaredAnnotation(PlatformServiceMethod.class);
 
         return ann == null ? mtd.getName() : ann.value();
     }
 
     /**
+     * Calls the target, measures and registers its duration.
+     *
+     * @param histogram Related metric.
+     * @param target    Target to call and measure.
+     */
+    private static <T> T measureCall(
+            HistogramMetricImpl histogram,
+            Callable<T> target
+    ) throws Exception {
+        long startTime = System.nanoTime();
+
+        try {
+            return target.call();
+        } finally {
+            histogram.value(System.nanoTime() - startTime);
+        }
+    }
+
+    /**
      * Invocation handler for service proxy.
      */
     private class ProxyInvocationHandler implements InvocationHandler {
-        /** Caller context provider. */
-        private final Supplier<ServiceCallContext> callCtxProvider;
+        /** Service call context attributes provider. */
+        private final Supplier<Map<String, Object>> callAttrsProvider;
 
         /**
-         * @param callCtxProvider Caller context provider.
+         * @param callAttrsProvider Service call context attributes provider.
          */
-        public ProxyInvocationHandler(@Nullable Supplier<ServiceCallContext> callCtxProvider) {
-            this.callCtxProvider = callCtxProvider;
+        public ProxyInvocationHandler(@Nullable Supplier<Map<String, Object>> callAttrsProvider) {
+            this.callAttrsProvider = callAttrsProvider;
         }
 
         /** {@inheritDoc} */
         @Override public Object invoke(Object proxy, final Method mtd, final Object[] args) throws Throwable {
-            return invokeMethod(mtd, args, callCtxProvider != null ? callCtxProvider.get() : null);
+            return invokeMethod(mtd, args, callAttrsProvider != null ? callAttrsProvider.get() : null);
         }
     }
 
     /**
      * Callable proxy class.
      */
-    private static class ServiceProxyCallable implements IgniteCallable<Object>, Externalizable {
+    private static class ServiceProxyCallable implements IgniteCallable<byte[]>, Externalizable {
         /** Serial version UID. */
         private static final long serialVersionUID = 0L;
 
@@ -479,8 +518,8 @@
         /** Args. */
         private Object[] args;
 
-        /** Service call context. */
-        private ServiceCallContext callCtx;
+        /** Service call context attributes. */
+        private Map<String, Object> callAttrs;
 
         /** Grid instance. */
         @IgniteInstanceResource
@@ -498,24 +537,24 @@
          * @param svcName Service name.
          * @param argTypes Argument types.
          * @param args Arguments for invocation.
-         * @param callCtx Service call context.
+         * @param callAttrs Service call context attributes.
          */
         private ServiceProxyCallable(
             String mtdName,
             String svcName,
             Class<?>[] argTypes,
             Object[] args,
-            @Nullable ServiceCallContext callCtx
+            @Nullable Map<String, Object> callAttrs
         ) {
             this.mtdName = mtdName;
             this.svcName = svcName;
             this.argTypes = argTypes;
             this.args = args;
-            this.callCtx = callCtx;
+            this.callAttrs = callAttrs;
         }
 
         /** {@inheritDoc} */
-        @Override public Object call() throws Exception {
+        @Override public byte[] call() throws Exception {
             ServiceContextImpl ctx = ignite.context().service().serviceContext(svcName);
 
             if (ctx == null || ctx.service() == null)
@@ -525,16 +564,25 @@
 
             Method mtd = ctx.method(key);
 
-            if (ctx.service() instanceof PlatformService && mtd == null)
-                return callPlatformService((PlatformService)ctx.service());
+            HistogramMetricImpl hist = ctx.isStatisticsEnabled() ? ctx.metrics().findMetric(mtd.getName()) : null;
+
+            Object res = hist == null ? callService(ctx, mtd) : measureCall(hist, () -> callService(ctx, mtd));
+
+            return U.marshal(ignite.configuration().getMarshaller(), res);
+        }
+
+        /** */
+        private Object callService(ServiceContextImpl svcCtx, Method mtd) throws Exception {
+            if (svcCtx.service() instanceof PlatformService && mtd == null)
+                return callPlatformService((PlatformService)svcCtx.service());
             else
-                return callService(ctx.service(), mtd);
+                return callOrdinaryService(svcCtx.service(), mtd);
         }
 
         /** */
         private Object callPlatformService(PlatformService srv) {
             try {
-                return srv.invokeMethod(mtdName, false, true, args, callCtx != null ? ((ServiceCallContextImpl)callCtx).values() : null);
+                return srv.invokeMethod(mtdName, false, true, args, callAttrs);
             }
             catch (PlatformNativeException ne) {
                 throw new ServiceProxyException(U.convertException(ne));
@@ -545,12 +593,12 @@
         }
 
         /** */
-        private Object callService(Service srv, Method mtd) throws Exception {
+        private Object callOrdinaryService(Service srv, Method mtd) throws Exception {
             if (mtd == null)
                 throw new GridServiceMethodNotFoundException(svcName, mtdName, argTypes);
 
             try {
-                return callServiceMethod(srv, mtd, args, callCtx);
+                return callServiceMethod(srv, mtd, args, callAttrs);
             }
             catch (InvocationTargetException e) {
                 throw new ServiceProxyException(e.getCause());
@@ -563,13 +611,7 @@
             U.writeString(out, mtdName);
             U.writeArray(out, argTypes);
             U.writeArray(out, args);
-
-            if (callCtx != null) {
-                out.writeBoolean(true);
-                callCtx.writeExternal(out);
-            }
-            else
-                out.writeBoolean(false);
+            U.writeMap(out, callAttrs);
         }
 
         /** {@inheritDoc} */
@@ -578,11 +620,7 @@
             mtdName = U.readString(in);
             argTypes = U.readClassArray(in);
             args = U.readArray(in);
-
-            if (in.readBoolean()) {
-                callCtx = new ServiceCallContextImpl();
-                callCtx.readExternal(in);
-            }
+            callAttrs = U.readMap(in);
         }
 
         /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
index 7991b74..4eaad94 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.service;
 
+import java.io.Externalizable;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -67,6 +69,7 @@
 import org.apache.ignite.internal.processors.cluster.ChangeGlobalStateMessage;
 import org.apache.ignite.internal.processors.cluster.DiscoveryDataClusterState;
 import org.apache.ignite.internal.processors.cluster.IgniteChangeGlobalStateSupport;
+import org.apache.ignite.internal.processors.metric.MetricRegistry;
 import org.apache.ignite.internal.processors.platform.services.PlatformService;
 import org.apache.ignite.internal.processors.security.OperationSecurityContext;
 import org.apache.ignite.internal.processors.security.SecurityContext;
@@ -84,7 +87,6 @@
 import org.apache.ignite.plugin.security.SecurityException;
 import org.apache.ignite.plugin.security.SecurityPermission;
 import org.apache.ignite.services.Service;
-import org.apache.ignite.services.ServiceCallContext;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.services.ServiceDeploymentException;
@@ -94,17 +96,22 @@
 import org.apache.ignite.spi.discovery.DiscoveryDataBag;
 import org.apache.ignite.spi.discovery.DiscoverySpi;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.apache.ignite.spi.systemview.view.ServiceView;
 import org.apache.ignite.thread.IgniteThreadFactory;
 import org.apache.ignite.thread.OomExceptionHandler;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static org.apache.ignite.configuration.DeploymentMode.ISOLATED;
 import static org.apache.ignite.configuration.DeploymentMode.PRIVATE;
 import static org.apache.ignite.events.EventType.EVT_NODE_JOINED;
 import static org.apache.ignite.internal.GridComponent.DiscoveryDataExchangeType.SERVICE_PROC;
+import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
 import static org.apache.ignite.internal.processors.security.SecurityUtils.nodeSecurityContext;
+import static org.apache.ignite.internal.util.IgniteUtils.allInterfaces;
 import static org.apache.ignite.plugin.security.SecurityPermission.SERVICE_DEPLOY;
 
 /**
@@ -127,6 +134,21 @@
     /** */
     public static final String SVCS_VIEW_DESC = "Services";
 
+    /** Base name domain for invocation metrics. */
+    private static final String SERVICE_METRIC_REGISTRY = "Services";
+
+    /** Description for the service method invocation metric. */
+    private static final String DESCRIPTION_OF_INVOCATION_METRIC_PREF = "Duration in milliseconds of ";
+
+    /** Default bounds of invocation histogram in nanoseconds. */
+    public static final long[] DEFAULT_INVOCATION_BOUNDS = new long[] {
+        NANOSECONDS.convert(1, MILLISECONDS),
+        NANOSECONDS.convert(10, MILLISECONDS),
+        NANOSECONDS.convert(50, MILLISECONDS),
+        NANOSECONDS.convert(200, MILLISECONDS),
+        NANOSECONDS.convert(1000, MILLISECONDS)
+    };
+
     /** Local service instances. */
     private final ConcurrentMap<IgniteUuid, Collection<ServiceContextImpl>> locServices = new ConcurrentHashMap<>();
 
@@ -317,6 +339,8 @@
 
         deployedServices.clear();
 
+        ctx.metric().remove(SERVICE_METRIC_REGISTRY);
+
         locServices.values().stream().flatMap(Collection::stream).forEach(srvcCtx -> {
             cancel(srvcCtx);
 
@@ -1020,7 +1044,7 @@
      * @param name Service name.
      * @param srvcCls Service class.
      * @param sticky Whether multi-node request should be done.
-     * @param callCtxProvider Caller context provider.
+     * @param callAttrsProvider Service call context attributes provider.
      * @param timeout If greater than 0 limits service acquire time. Cannot be negative.
      * @param <T> Service interface type.
      * @return The proxy of a service by its name and class.
@@ -1031,42 +1055,13 @@
         String name,
         Class<? super T> srvcCls,
         boolean sticky,
-        @Nullable Supplier<ServiceCallContext> callCtxProvider,
-        long timeout
+        @Nullable Supplier<Map<String, Object>> callAttrsProvider,
+        long timeout,
+        boolean keepBinary
     ) throws IgniteException {
         ctx.security().authorize(name, SecurityPermission.SERVICE_INVOKE);
 
-        if (hasLocalNode(prj)) {
-            ServiceContextImpl ctx = serviceContext(name);
-
-            if (ctx != null) {
-                Service srvc = ctx.service();
-
-                if (srvc != null && callCtxProvider == null) {
-                    if (srvcCls.isAssignableFrom(srvc.getClass()))
-                        return (T)srvc;
-                    else if (!PlatformService.class.isAssignableFrom(srvc.getClass())) {
-                        throw new IgniteException("Service does not implement specified interface [srvcCls="
-                                + srvcCls.getName() + ", srvcCls=" + srvc.getClass().getName() + ']');
-                    }
-                }
-            }
-        }
-
-        return new GridServiceProxy<T>(prj, name, srvcCls, sticky, timeout, ctx, callCtxProvider).proxy();
-    }
-
-    /**
-     * @param prj Grid nodes projection.
-     * @return Whether given projection contains any local node.
-     */
-    private boolean hasLocalNode(ClusterGroup prj) {
-        for (ClusterNode n : prj.nodes()) {
-            if (n.isLocal())
-                return true;
-        }
-
-        return false;
+        return new GridServiceProxy<T>(prj, name, srvcCls, sticky, timeout, ctx, callAttrsProvider, keepBinary).proxy();
     }
 
     /**
@@ -1267,7 +1262,8 @@
                         UUID.randomUUID(),
                         cacheName,
                         affKey,
-                        Executors.newSingleThreadExecutor(threadFactory));
+                        Executors.newSingleThreadExecutor(threadFactory),
+                        cfg.isStatisticsEnabled());
 
                     ctxs.add(srvcCtx);
 
@@ -1276,6 +1272,8 @@
             }
         }
 
+        ReadOnlyMetricRegistry invocationMetrics = null;
+
         for (final ServiceContextImpl srvcCtx : toInit) {
             final Service srvc;
 
@@ -1302,6 +1300,13 @@
                 log.info("Starting service instance [name=" + srvcCtx.name() + ", execId=" +
                     srvcCtx.executionId() + ']');
 
+            if (cfg.isStatisticsEnabled()) {
+                if (invocationMetrics == null)
+                    invocationMetrics = createServiceMetrics(srvcCtx);
+
+                srvcCtx.metrics(invocationMetrics);
+            }
+
             // Start service in its own thread.
             final ExecutorService exe = srvcCtx.executor();
 
@@ -1388,14 +1393,20 @@
      * @param ctxs Contexts to cancel.
      * @param cancelCnt Number of contexts to cancel.
      */
-    private void cancel(Iterable<ServiceContextImpl> ctxs, int cancelCnt) {
+    private void cancel(Collection<ServiceContextImpl> ctxs, int cancelCnt) {
         for (Iterator<ServiceContextImpl> it = ctxs.iterator(); it.hasNext(); ) {
-            cancel(it.next());
+            ServiceContextImpl svcCtx = it.next();
+
+            cancel(svcCtx);
 
             it.remove();
 
-            if (--cancelCnt == 0)
+            if (--cancelCnt == 0) {
+                if (ctxs.isEmpty())
+                    ctx.metric().remove(serviceMetricRegistryName(svcCtx.name()));
+
                 break;
+            }
         }
     }
 
@@ -1966,4 +1977,43 @@
 
         return null;
     }
+
+    /**
+     * Creates metrics registry for the invocation histograms.
+     *
+     * @param srvcCtx ServiceContext.
+     * @return Created metric registry.
+     */
+    private ReadOnlyMetricRegistry createServiceMetrics(ServiceContextImpl srvcCtx) {
+        MetricRegistry metricRegistry = ctx.metric().registry(serviceMetricRegistryName(srvcCtx.name()));
+
+        for (Class<?> itf : allInterfaces(srvcCtx.service().getClass())) {
+            for (Method mtd : itf.getMethods()) {
+                if (metricIgnored(mtd.getDeclaringClass()))
+                    continue;
+
+                metricRegistry.histogram(mtd.getName(), DEFAULT_INVOCATION_BOUNDS, DESCRIPTION_OF_INVOCATION_METRIC_PREF +
+                    '\'' + mtd.getName() + "()'");
+            }
+        }
+
+        return metricRegistry;
+    }
+
+    /**
+     * @return {@code True} if metrics should not be created for this class or interface.
+     */
+    private static boolean metricIgnored(Class<?> cls) {
+        return Service.class.equals(cls) || Externalizable.class.equals(cls) || PlatformService.class.equals(cls);
+    }
+
+    /**
+     * Gives proper name for service metric registry.
+     *
+     * @param srvcName Name of the service.
+     * @return registry name for service {@code srvcName}.
+     */
+    static String serviceMetricRegistryName(String srvcName) {
+        return metricName(SERVICE_METRIC_REGISTRY, srvcName);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
index e0add26..7d34665 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
@@ -64,6 +64,7 @@
         this.srvcBytes = srvcBytes;
         srvc = cfg.getService();
         srvcClsName = srvc.getClass().getName();
+        isStatisticsEnabled = cfg.isStatisticsEnabled();
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextHolder.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextHolder.java
index a9a449c..efcfa7e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextHolder.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextHolder.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.processors.service;
 
-import org.apache.ignite.services.ServiceCallContext;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -25,19 +24,19 @@
  */
 public class ServiceCallContextHolder {
     /** Service call context of the current thread. */
-    private static final ThreadLocal<ServiceCallContext> locCallCtx = new ThreadLocal<>();
+    private static final ThreadLocal<ServiceCallContextImpl> locCallCtx = new ThreadLocal<>();
 
     /**
      * @return Service call context of the current thread.
      */
-    @Nullable public static ServiceCallContext current() {
+    @Nullable public static ServiceCallContextImpl current() {
         return locCallCtx.get();
     }
 
     /**
      * @param callCtx Service call context of the current thread.
      */
-    static void current(@Nullable ServiceCallContext callCtx) {
+    static void current(@Nullable ServiceCallContextImpl callCtx) {
         if (callCtx != null)
             locCallCtx.set(callCtx);
         else
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextImpl.java
index ab53c12..c9dc6d0 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceCallContextImpl.java
@@ -17,32 +17,17 @@
 
 package org.apache.ignite.internal.processors.service;
 
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-import java.util.HashMap;
 import java.util.Map;
-import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.services.ServiceCallContext;
 
 /**
  * Service call context implementation.
  */
 public class ServiceCallContextImpl implements ServiceCallContext {
-    /** */
-    private static final long serialVersionUID = 0L;
-
     /** Service call context attributes. */
     private Map<String, Object> attrs;
 
     /**
-     * Default contructor.
-     */
-    public ServiceCallContextImpl() {
-        attrs = new HashMap<>();
-    }
-
-    /**
      * @param attrs Service call context attributes.
      */
     public ServiceCallContextImpl(Map<String, Object> attrs) {
@@ -59,20 +44,10 @@
         return (byte[])attrs.get(name);
     }
 
-    /** {@inheritDoc} */
-    @Override public void writeExternal(ObjectOutput out) throws IOException {
-        U.writeMap(out, attrs);
-    }
-
-    /** {@inheritDoc} */
-    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-        attrs = U.readMap(in);
-    }
-
     /**
      * @return Service call context attributes.
      */
-    Map<String, Object> values() {
+    public Map<String, Object> values() {
         return attrs;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
index 38acfce..a5aa112 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
@@ -28,6 +28,7 @@
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceCallContext;
 import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -60,6 +61,9 @@
     /** Methods reflection cache. */
     private final ConcurrentMap<GridServiceMethodReflectKey, Method> mtds = new ConcurrentHashMap<>();
 
+    /** Invocation metrics. */
+    private ReadOnlyMetricRegistry metrics;
+
     /** Service. */
     @GridToStringExclude
     private volatile Service svc;
@@ -67,23 +71,29 @@
     /** Cancelled flag. */
     private volatile boolean isCancelled;
 
+    /** Service statistics flag. */
+    private final boolean isStatisticsEnabled;
+
     /**
      * @param name Service name.
      * @param execId Execution ID.
      * @param cacheName Cache name.
      * @param affKey Affinity key.
      * @param exe Executor service.
+     * @param statisticsEnabled Service statistics flag.
      */
     ServiceContextImpl(String name,
         UUID execId,
         String cacheName,
         Object affKey,
-        ExecutorService exe) {
+        ExecutorService exe,
+        boolean statisticsEnabled) {
         this.name = name;
         this.execId = execId;
         this.cacheName = cacheName;
         this.affKey = affKey;
         this.exe = exe;
+        this.isStatisticsEnabled = statisticsEnabled;
     }
 
     /** {@inheritDoc} */
@@ -133,6 +143,29 @@
     }
 
     /**
+     * @return Invocation metrics.
+     */
+    @Nullable ReadOnlyMetricRegistry metrics() {
+        return metrics;
+    }
+
+    /**
+     * Sets the invocation metrics.
+     *
+     * @return {@code this}.
+     */
+    ServiceContextImpl metrics(ReadOnlyMetricRegistry metrics) {
+        this.metrics = metrics;
+
+        return this;
+    }
+
+    /** @return {@code True} if statistics is enabled for this service. {@code False} otherwise. */
+    boolean isStatisticsEnabled() {
+        return isStatisticsEnabled;
+    }
+
+    /**
      * @param key Method key.
      * @return Method.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceDescriptorImpl.java
deleted file mode 100644
index 8d1d9a6..0000000
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceDescriptorImpl.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.processors.service;
-
-import java.util.Map;
-import java.util.UUID;
-import org.apache.ignite.IgniteException;
-import org.apache.ignite.internal.util.tostring.GridToStringInclude;
-import org.apache.ignite.internal.util.typedef.internal.S;
-import org.apache.ignite.services.Service;
-import org.apache.ignite.services.ServiceConfiguration;
-import org.apache.ignite.services.ServiceDescriptor;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Service descriptor.
- *
- * @deprecated This implementation is based on {@code GridServiceDeployment} which has been deprecated because of
- * services internals use messages for deployment management instead of the utility cache, since Ignite 2.8.
- */
-@Deprecated
-public class ServiceDescriptorImpl implements ServiceDescriptor {
-    /** */
-    private static final long serialVersionUID = 0L;
-
-    /** Configuration. */
-    @GridToStringInclude
-    private final GridServiceDeployment dep;
-
-    /** Topology snapshot. */
-    @GridToStringInclude
-    private Map<UUID, Integer> top;
-
-    /**
-     * @param dep Deployment.
-     */
-    public ServiceDescriptorImpl(GridServiceDeployment dep) {
-        this.dep = dep;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String name() {
-        return dep.configuration().getName();
-    }
-
-    /** {@inheritDoc} */
-    @Override public Class<? extends Service> serviceClass() {
-        ServiceConfiguration cfg = dep.configuration();
-
-        if (cfg instanceof LazyServiceConfiguration) {
-            String clsName = ((LazyServiceConfiguration)cfg).serviceClassName();
-
-            try {
-                return (Class<? extends Service>)Class.forName(clsName);
-            }
-            catch (ClassNotFoundException e) {
-                throw new IgniteException("Failed to find service class: " + clsName, e);
-            }
-        }
-        else
-            return dep.configuration().getService().getClass();
-    }
-
-    /** {@inheritDoc} */
-    @Override public int totalCount() {
-        return dep.configuration().getTotalCount();
-    }
-
-    /** {@inheritDoc} */
-    @Override public int maxPerNodeCount() {
-        return dep.configuration().getMaxPerNodeCount();
-    }
-
-    /** {@inheritDoc} */
-    @Nullable @Override public String cacheName() {
-        return dep.configuration().getCacheName();
-    }
-
-    /** {@inheritDoc} */
-    @Nullable @Override public <K> K affinityKey() {
-        return (K)dep.configuration().getAffinityKey();
-    }
-
-    /** {@inheritDoc} */
-    @Override public UUID originNodeId() {
-        return dep.nodeId();
-    }
-
-    /** {@inheritDoc} */
-    @Override public Map<UUID, Integer> topologySnapshot() {
-        return top;
-    }
-
-    /**
-     * @param top Topology snapshot.
-     */
-    void topologySnapshot(Map<UUID, Integer> top) {
-        this.top = top;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return S.toString(ServiceDescriptorImpl.class, this);
-    }
-}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/session/GridTaskSessionProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/session/GridTaskSessionProcessor.java
index 3feeb65..a46b731 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/session/GridTaskSessionProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/session/GridTaskSessionProcessor.java
@@ -65,6 +65,8 @@
     }
 
     /**
+     * Creates task session.
+     *
      * @param sesId Session ID.
      * @param taskNodeId Task node ID.
      * @param taskName Task name.
@@ -79,6 +81,7 @@
      * @param fullSup {@code True} to enable distributed session attributes and checkpoints.
      * @param internal {@code True} in case of internal task.
      * @param execName Custom executor name.
+     * @param login User who created the session, {@code null} if security is not enabled.
      * @return New session if one did not exist, or existing one.
      */
     public GridTaskSessionImpl createTaskSession(
@@ -95,7 +98,9 @@
         Map<Object, Object> attrs,
         boolean fullSup,
         boolean internal,
-        @Nullable String execName) {
+        @Nullable String execName,
+        @Nullable Object login
+    ) {
         if (!fullSup) {
             return new GridTaskSessionImpl(
                 taskNodeId,
@@ -112,7 +117,9 @@
                 ctx,
                 false,
                 internal,
-                execName);
+                execName,
+                login
+            );
         }
 
         while (true) {
@@ -136,7 +143,10 @@
                         ctx,
                         true,
                         internal,
-                        execName));
+                        execName,
+                        login
+                    )
+                );
 
                 if (old != null)
                     ses = old;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskEventListener.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskEventListener.java
index 6c4556b..377cf48 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskEventListener.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskEventListener.java
@@ -20,6 +20,7 @@
 import java.util.EventListener;
 import java.util.UUID;
 import org.apache.ignite.internal.GridJobSiblingImpl;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Listener for task events.
@@ -28,7 +29,14 @@
     /**
      * @param worker Started grid task worker.
      */
-    public void onTaskStarted(GridTaskWorker<?, ?> worker);
+    void onTaskStarted(GridTaskWorker<?, ?> worker);
+
+    /**
+     * Callback on splitting the task into jobs.
+     *
+     * @param worker Grid task worker.
+     */
+    void onJobsMapped(GridTaskWorker<?, ?> worker);
 
     /**
      * @param worker Grid task worker.
@@ -41,16 +49,19 @@
      * @param sib Job sibling.
      * @param nodeId Failover node ID.
      */
-    public void onJobFailover(GridTaskWorker<?, ?> worker, GridJobSiblingImpl sib, UUID nodeId);
+    void onJobFailover(GridTaskWorker<?, ?> worker, GridJobSiblingImpl sib, UUID nodeId);
 
     /**
      * @param worker Grid task worker.
      * @param sib Job sibling.
      */
-    public void onJobFinished(GridTaskWorker<?, ?> worker, GridJobSiblingImpl sib);
+    void onJobFinished(GridTaskWorker<?, ?> worker, GridJobSiblingImpl sib);
 
     /**
+     * Callback on finish of task execution.
+     *
      * @param worker Task worker for finished grid task.
+     * @param err Reason for the failure of the task, {@code null} if the task completed successfully.
      */
-    public void onTaskFinished(GridTaskWorker<?, ?> worker);
+    void onTaskFinished(GridTaskWorker<?, ?> worker, @Nullable Throwable err);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskProcessor.java
index da7f6ca..0258114 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskProcessor.java
@@ -25,6 +25,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.RejectedExecutionException;
@@ -38,6 +39,7 @@
 import org.apache.ignite.compute.ComputeTaskFuture;
 import org.apache.ignite.compute.ComputeTaskMapAsync;
 import org.apache.ignite.compute.ComputeTaskName;
+import org.apache.ignite.compute.ComputeTaskSession;
 import org.apache.ignite.compute.ComputeTaskSessionFullSupport;
 import org.apache.ignite.events.DiscoveryEvent;
 import org.apache.ignite.events.Event;
@@ -55,6 +57,7 @@
 import org.apache.ignite.internal.IgniteClientDisconnectedCheckedException;
 import org.apache.ignite.internal.IgniteDeploymentCheckedException;
 import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.NodeStoppingException;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.compute.ComputeTaskCancelledCheckedException;
 import org.apache.ignite.internal.managers.communication.GridIoManager;
@@ -65,8 +68,12 @@
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
 import org.apache.ignite.internal.processors.cluster.IgniteChangeGlobalStateSupport;
+import org.apache.ignite.internal.processors.job.ComputeJobStatusEnum;
 import org.apache.ignite.internal.processors.metric.MetricRegistry;
 import org.apache.ignite.internal.processors.metric.impl.LongAdderMetric;
+import org.apache.ignite.internal.processors.task.monitor.ComputeGridMonitor;
+import org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatus;
+import org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusSnapshot;
 import org.apache.ignite.internal.util.GridConcurrentFactory;
 import org.apache.ignite.internal.util.GridSpinReadWriteLock;
 import org.apache.ignite.internal.util.lang.GridPeerDeployAware;
@@ -87,6 +94,7 @@
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static java.util.Collections.emptyMap;
 import static org.apache.ignite.events.EventType.EVT_MANAGEMENT_TASK_STARTED;
 import static org.apache.ignite.events.EventType.EVT_NODE_FAILED;
 import static org.apache.ignite.events.EventType.EVT_NODE_LEFT;
@@ -160,6 +168,19 @@
     private final boolean isPersistenceEnabled;
 
     /**
+     * Task statuses update monitors.
+     * Guarded by {@link #lock}.
+     */
+    private final Collection<ComputeGridMonitor> taskStatusMonitors = ConcurrentHashMap.newKeySet();
+
+    /**
+     * Snapshots of task statuses.
+     * Mapping: {@link ComputeTaskSession#getId} -> task status.
+     * Guarded by {@link #lock}.
+     */
+    private final ConcurrentMap<IgniteUuid, ComputeTaskStatusSnapshot> taskStatusSnapshots = new ConcurrentHashMap<>();
+
+    /**
      * @param ctx Kernal context.
      */
     public GridTaskProcessor(GridKernalContext ctx) {
@@ -758,11 +779,13 @@
             topPred,
             startTime,
             endTime,
-            Collections.<ComputeJobSibling>emptyList(),
-            Collections.emptyMap(),
+            Collections.emptyList(),
+            emptyMap(),
             fullSup,
             internal,
-            execName);
+            execName,
+            ctx.security().enabled() ? ctx.security().securityContext().subject().login() : null
+        );
 
         ComputeTaskInternalFuture<R> fut = new ComputeTaskInternalFuture<>(ses, ctx);
 
@@ -1034,7 +1057,7 @@
 
                 UUID nodeId = sib.nodeId();
 
-                if (!nodeId.equals(locNodeId) && !sib.isJobDone() && !rcvrs.contains(nodeId))
+                if (!nodeId.equals(locNodeId) && !sib.isJobDone())
                     rcvrs.add(nodeId);
             }
         }
@@ -1053,6 +1076,8 @@
             ctx.event().record(evt);
         }
 
+        notifyTaskStatusMonitors(ComputeTaskStatus.snapshot(ses), false);
+
         IgniteCheckedException ex = null;
 
         // Every job gets an individual message to keep track of ghost requests.
@@ -1061,8 +1086,12 @@
 
             UUID nodeId = sib.nodeId();
 
-            // Pair can be null if job is finished.
-            if (rcvrs.remove(nodeId)) {
+            if (locNodeId.equals(nodeId)) {
+                // Local job notification.
+                ctx.job().onChangeTaskAttributes(ses.getId(), s.getJobId(), attrs);
+            }
+            else if (rcvrs.remove(nodeId)) {
+                // Pair can be null if job is finished.
                 ClusterNode node = ctx.discovery().node(nodeId);
 
                 // Check that node didn't change (it could happen in case of failover).
@@ -1071,9 +1100,10 @@
 
                     GridTaskSessionRequest req = new GridTaskSessionRequest(
                         ses.getId(),
-                        null,
+                        s.getJobId(),
                         loc ? null : U.marshal(marsh, attrs),
-                        attrs);
+                        attrs
+                    );
 
                     // Make sure to go through IO manager always, since order
                     // should be preserved here.
@@ -1274,6 +1304,17 @@
             // Register for timeout notifications.
             if (worker.endTime() < Long.MAX_VALUE)
                 ctx.timeout().addTimeoutObject(worker);
+
+            GridTaskSessionImpl session = worker.getSession();
+
+            notifyTaskStatusMonitors(ComputeTaskStatus.snapshot(session), false);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void onJobsMapped(GridTaskWorker<?, ?> worker) {
+            GridTaskSessionImpl session = worker.getSession();
+
+            notifyTaskStatusMonitors(ComputeTaskStatus.snapshot(session), false);
         }
 
         /** {@inheritDoc} */
@@ -1317,7 +1358,7 @@
         }
 
         /** {@inheritDoc} */
-        @Override public void onTaskFinished(GridTaskWorker<?, ?> worker) {
+        @Override public void onTaskFinished(GridTaskWorker<?, ?> worker, @Nullable Throwable err) {
             GridTaskSessionImpl ses = worker.getSession();
 
             if (ses.isFullSupport()) {
@@ -1366,6 +1407,8 @@
                     U.currentTimeMillis() - ses.getStartTime(),
                     worker.affPartId());
             }
+
+            notifyTaskStatusMonitors(ComputeTaskStatus.onFinishTask(worker.getSession(), err), true);
         }
     }
 
@@ -1552,4 +1595,97 @@
             ? ", task name: " + task.getSession().getTaskName()
             : "";
     }
+
+    /**
+     * Subscription to update the status of tasks.
+     *
+     * <p>NOTE: {@link ComputeGridMonitor#processStatusSnapshots} will be called only on subscription,
+     * then only {@link ComputeGridMonitor#processStatusChange} will be called.
+     *
+     * @param monitor Task status update monitor.
+     * @throws NodeStoppingException If the node is stopped.
+     */
+    public void listenStatusUpdates(ComputeGridMonitor monitor) throws NodeStoppingException {
+        lock.writeLock();
+
+        try {
+            if (stopping)
+                throw new NodeStoppingException("Failed to add monitor due to grid shutdown: " + monitor);
+
+            taskStatusMonitors.add(monitor);
+
+            try {
+                monitor.processStatusSnapshots(taskStatusSnapshots.values());
+            }
+            catch (Throwable t) {
+                log.error("Error processing snapshots of task statuses: " + monitor, t);
+            }
+        }
+        finally {
+            lock.writeUnlock();
+        }
+    }
+
+    /**
+     * Unsubscribe to update the status of tasks.
+     *
+     * @param monitor Task status update monitor.
+     */
+    public void stopListenStatusUpdates(ComputeGridMonitor monitor) {
+        lock.writeLock();
+
+        try {
+            taskStatusMonitors.remove(monitor);
+        }
+        finally {
+            lock.writeUnlock();
+        }
+    }
+
+    /**
+     * Guarded by {@link #lock} for atomic update of the {@link #taskStatusSnapshots}
+     * and notifying {@link #taskStatusMonitors} about task changes.
+     *
+     * @param snapshotChanges Changes to task status.
+     * @param remove {@code True} if it is necessary to remove the {@code snapshotChanges} from
+     *      {@link #taskStatusSnapshots}, otherwise it will be updated.
+     */
+    private void notifyTaskStatusMonitors(ComputeTaskStatusSnapshot snapshotChanges, boolean remove) {
+        lock.readLock();
+
+        try {
+            if (remove)
+                taskStatusSnapshots.remove(snapshotChanges.sessionId());
+            else
+                taskStatusSnapshots.put(snapshotChanges.sessionId(), snapshotChanges);
+
+            for (ComputeGridMonitor monitor : taskStatusMonitors) {
+                try {
+                    monitor.processStatusChange(snapshotChanges);
+                }
+                catch (Throwable t) {
+                    log.error("Error processing task status diff: " + monitor, t);
+                }
+            }
+        }
+        finally {
+            lock.readUnlock();
+        }
+    }
+
+    /**
+     * Collects statistics on jobs locally, only for those jobs that have
+     * already sent a response or are being executed locally.
+     *
+     * @param sesId Task session ID.
+     * @return Job statistics for the task. Mapping: Job status -> count of jobs.
+     */
+    public Map<ComputeJobStatusEnum, Long> jobStatuses(IgniteUuid sesId) {
+        GridTaskWorker<?, ?> taskWorker = tasks.get(sesId);
+
+        if (taskWorker == null)
+            return emptyMap();
+        else
+            return taskWorker.jobStatuses();
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskWorker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskWorker.java
index 6cdcd79..4055d6c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskWorker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/GridTaskWorker.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -67,6 +68,7 @@
 import org.apache.ignite.internal.managers.deployment.GridDeployment;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.closure.AffinityTask;
+import org.apache.ignite.internal.processors.job.ComputeJobStatusEnum;
 import org.apache.ignite.internal.processors.service.GridServiceNotFoundException;
 import org.apache.ignite.internal.processors.timeout.GridTimeoutObject;
 import org.apache.ignite.internal.util.lang.GridPlainRunnable;
@@ -86,6 +88,8 @@
 import org.apache.ignite.resources.TaskContinuousMapperResource;
 import org.jetbrains.annotations.Nullable;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
 import static org.apache.ignite.compute.ComputeJobResultPolicy.FAILOVER;
 import static org.apache.ignite.compute.ComputeJobResultPolicy.WAIT;
 import static org.apache.ignite.events.EventType.EVT_JOB_FAILED_OVER;
@@ -100,6 +104,9 @@
 import static org.apache.ignite.internal.GridTopic.TOPIC_JOB_CANCEL;
 import static org.apache.ignite.internal.managers.communication.GridIoPolicy.MANAGEMENT_POOL;
 import static org.apache.ignite.internal.managers.communication.GridIoPolicy.PUBLIC_POOL;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.CANCELLED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FAILED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FINISHED;
 import static org.apache.ignite.internal.processors.task.GridTaskThreadContextKey.TC_IO_POLICY;
 import static org.apache.ignite.internal.processors.task.GridTaskThreadContextKey.TC_NO_FAILOVER;
 import static org.apache.ignite.internal.processors.task.GridTaskThreadContextKey.TC_NO_RESULT_CACHE;
@@ -158,7 +165,7 @@
     /** */
     private final GridTaskEventListener evtLsnr;
 
-    /** */
+    /** Guarded by {@link #mux}. */
     private Map<IgniteUuid, GridJobResultImpl> jobRes;
 
     /** */
@@ -499,8 +506,7 @@
             ses.setClassLoader(dep.classLoader());
 
             // Nodes are ignored by affinity tasks.
-            final List<ClusterNode> shuffledNodes =
-                affCacheIds == null ? getTaskTopology() : Collections.<ClusterNode>emptyList();
+            final List<ClusterNode> shuffledNodes = affCacheIds == null ? getTaskTopology() : emptyList();
 
             // Load balancer.
             ComputeLoadBalancer balancer = ctx.loadBalancing().getLoadBalancer(ses, shuffledNodes);
@@ -513,16 +519,15 @@
             // Inject resources.
             ctx.resource().inject(dep, task, ses, balancer, mapper);
 
-            Map<? extends ComputeJob, ClusterNode> mappedJobs = U.wrapThreadLoader(dep.classLoader(),
-                new Callable<Map<? extends ComputeJob, ClusterNode>>() {
-                    @Override public Map<? extends ComputeJob, ClusterNode> call() {
-                        return task.map(shuffledNodes, arg);
-                    }
-                });
+            Map<? extends ComputeJob, ClusterNode> mappedJobs = U.wrapThreadLoader(
+                dep.classLoader(),
+                (Callable<Map<? extends ComputeJob, ClusterNode>>)() -> task.map(shuffledNodes, arg)
+            );
 
-            if (log.isDebugEnabled())
+            if (log.isDebugEnabled()) {
                 log.debug("Mapped task jobs to nodes [jobCnt=" + (mappedJobs != null ? mappedJobs.size() : 0) +
                     ", mappedJobs=" + mappedJobs + ", ses=" + ses + ']');
+            }
 
             if (F.isEmpty(mappedJobs)) {
                 synchronized (mux) {
@@ -634,6 +639,10 @@
             }
         }
 
+        ses.jobNodes(F.viewReadOnly(jobs.values(), F.node2id()));
+
+        evtLsnr.onJobsMapped(this);
+
         // Set mapped flag.
         ses.onMapped();
 
@@ -854,7 +863,7 @@
                 List<ComputeJobResult> results;
 
                 if (!resCache)
-                    results = Collections.emptyList();
+                    results = emptyList();
                 else {
                     synchronized (mux) {
                         results = getRemoteResults();
@@ -1640,7 +1649,7 @@
                 recordTaskEvent(EVT_TASK_FAILED, "Task failed.");
 
             // Clean resources prior to finishing future.
-            evtLsnr.onTaskFinished(this);
+            evtLsnr.onTaskFinished(this, e);
 
             if (cancelChildren)
                 cancelChildren();
@@ -1674,6 +1683,57 @@
         return affPartId;
     }
 
+    /**
+     * Collects statistics on jobs locally, only for those jobs that have
+     * already sent a response or are being executed locally.
+     *
+     * @return Job statistics for the task. Mapping: Job status -> count of jobs.
+     */
+    Map<ComputeJobStatusEnum, Long> jobStatuses() {
+        List<GridJobResultImpl> jobResults = null;
+
+        synchronized (mux) {
+            if (jobRes != null)
+                jobResults = new ArrayList<>(jobRes.values());
+        }
+
+        // Jobs have not been mapped yet.
+        if (F.isEmpty(jobResults))
+            return emptyMap();
+
+        UUID locNodeId = ctx.localNodeId();
+
+        boolean getLocJobStatistics = false;
+
+        Map<ComputeJobStatusEnum, Long> res = new EnumMap<>(ComputeJobStatusEnum.class);
+
+        for (GridJobResultImpl jobResult : jobResults) {
+            if (jobResult.hasResponse()) {
+                ComputeJobStatusEnum jobStatus;
+
+                if (jobResult.isCancelled())
+                    jobStatus = CANCELLED;
+                else if (jobResult.getException() != null)
+                    jobStatus = FAILED;
+                else
+                    jobStatus = FINISHED;
+
+                res.merge(jobStatus, 1L, Long::sum);
+            }
+            else if (!getLocJobStatistics && locNodeId.equals(jobResult.getNode().id()))
+                getLocJobStatistics = true;
+        }
+
+        if (getLocJobStatistics) {
+            Map<ComputeJobStatusEnum, Long> jobStatuses = ctx.job().jobStatuses(getTaskSessionId());
+
+            for (Map.Entry<ComputeJobStatusEnum, Long> e : jobStatuses.entrySet())
+                res.merge(e.getKey(), e.getValue(), Long::sum);
+        }
+
+        return res;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object obj) {
         if (this == obj)
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeGridMonitor.java
similarity index 60%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeGridMonitor.java
index a244658..82bce5f 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeGridMonitor.java
@@ -15,16 +15,25 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.task.monitor;
 
-import org.apache.ignite.cache.CacheMode;
+import java.util.Collection;
 
 /**
- *
+ * Monitor for updating task statuses.
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
-    /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
-    }
+public interface ComputeGridMonitor {
+    /**
+     * Processing task snapshots.
+     *
+     * @param snapshots Snapshots of tasks.
+     */
+    void processStatusSnapshots(Collection<ComputeTaskStatusSnapshot> snapshots);
+
+    /**
+     * Processing a change in a task.
+     *
+     * @param snapshot Snapshot of the task.
+     */
+    void processStatusChange(ComputeTaskStatusSnapshot snapshot);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatus.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatus.java
new file mode 100644
index 0000000..ef4e40f
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatus.java
@@ -0,0 +1,227 @@
+/*
+ * 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.task.monitor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.internal.GridTaskSessionImpl;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.FAILED;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.FINISHED;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.RUNNING;
+
+/**
+ * Task status container.
+ *
+ * @see ComputeTaskStatusSnapshot
+ */
+public class ComputeTaskStatus implements ComputeTaskStatusSnapshot {
+    /** Session ID of the task being executed. */
+    private final IgniteUuid sessionId;
+
+    /** Status of the task. */
+    private final ComputeTaskStatusEnum status;
+
+    /** Task name of the task this session belongs to. */
+    private final String taskName;
+
+    /** ID of the node on which task execution originated. */
+    private final UUID originatingNodeId;
+
+    /** Start of computation time for the task. */
+    private final long startTime;
+
+    /** End of computation time for the task. */
+    private final long endTime;
+
+    /** Nodes IDs on which the task jobs will execute. */
+    private final List<UUID> jobNodes;
+
+    /** All session attributes. */
+    private final Map<?, ?> attributes;
+
+    /** Reason for the failure of the task. */
+    @Nullable private final Throwable failReason;
+
+    /** Availability of changing task attributes. */
+    private final boolean fullSupport;
+
+    /** User who created the task, {@code null} if security is not available. */
+    @Nullable private Object createdBy;
+
+    /** Internal task flag. */
+    private final boolean internal;
+
+    /**
+     * Constructor for a new task.
+     *
+     * @param sessionId Session ID of the task being executed.
+     * @param status Status of the task.
+     * @param taskName Task name of the task this session belongs to.
+     * @param originatingNodeId ID of the node on which task execution originated.
+     * @param startTime Start of computation time for the task.
+     * @param endTime End of computation time for the task.
+     * @param jobNodes Nodes IDs on which the task jobs will execute.
+     * @param attributes All session attributes.
+     * @param failReason Reason for the failure of the task.
+     * @param fullSupport Availability of changing task attributes.
+     * @param createdBy User who created the task, {@code null} if security is not available.
+     * @param internal Internal task flag.
+     */
+    private ComputeTaskStatus(
+        IgniteUuid sessionId,
+        ComputeTaskStatusEnum status,
+        String taskName,
+        UUID originatingNodeId,
+        long startTime,
+        long endTime,
+        List<UUID> jobNodes,
+        Map<?, ?> attributes,
+        @Nullable Throwable failReason,
+        boolean fullSupport,
+        @Nullable Object createdBy,
+        boolean internal
+    ) {
+        this.sessionId = sessionId;
+        this.status = status;
+        this.taskName = taskName;
+        this.originatingNodeId = originatingNodeId;
+        this.startTime = startTime;
+        this.endTime = endTime;
+        this.jobNodes = F.isEmpty(jobNodes) ? emptyList() : jobNodes;
+        this.attributes = F.isEmpty(attributes) ? emptyMap() : attributes;
+        this.failReason = failReason;
+        this.fullSupport = fullSupport;
+        this.createdBy = createdBy;
+        this.internal = internal;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteUuid sessionId() {
+        return sessionId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String taskName() {
+        return taskName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public UUID originatingNodeId() {
+        return originatingNodeId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public long startTime() {
+        return startTime;
+    }
+
+    /** {@inheritDoc} */
+    @Override public long endTime() {
+        return endTime;
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<UUID> jobNodes() {
+        return jobNodes;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Map<?, ?> attributes() {
+        return attributes;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ComputeTaskStatusEnum status() {
+        return status;
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable Throwable failReason() {
+        return failReason;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean fullSupport() {
+        return fullSupport;
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable Object createBy() {
+        return createdBy;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean internal() {
+        return internal;
+    }
+
+    /**
+     * Creates the status of a task that is in progress.
+     *
+     * @param sessionImp Task session.
+     * @return New instance.
+     */
+    public static ComputeTaskStatus snapshot(GridTaskSessionImpl sessionImp) {
+        return new ComputeTaskStatus(
+            sessionImp.getId(),
+            RUNNING,
+            sessionImp.getTaskName(),
+            sessionImp.getTaskNodeId(),
+            sessionImp.getStartTime(),
+            0L,
+            sessionImp.jobNodesSafeCopy(),
+            sessionImp.attributesSafeCopy(),
+            null,
+            sessionImp.isFullSupport(),
+            sessionImp.login(),
+            sessionImp.isInternal()
+        );
+    }
+
+    /**
+     * Creates a task status on finishing task.
+     *
+     * @param sessionImp Task session.
+     * @param err – Reason for the failure of the task, null if the task completed successfully.
+     * @return New instance.
+     */
+    public static ComputeTaskStatus onFinishTask(GridTaskSessionImpl sessionImp, @Nullable Throwable err) {
+        return new ComputeTaskStatus(
+            sessionImp.getId(),
+            err == null ? FINISHED : FAILED,
+            sessionImp.getTaskName(),
+            sessionImp.getTaskNodeId(),
+            sessionImp.getStartTime(),
+            U.currentTimeMillis(),
+            sessionImp.jobNodesSafeCopy(),
+            sessionImp.attributesSafeCopy(),
+            err,
+            sessionImp.isFullSupport(),
+            sessionImp.login(),
+            sessionImp.isInternal()
+        );
+    }
+}
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusEnum.java
similarity index 74%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusEnum.java
index a5b6d8f..06faa8b 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusEnum.java
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+package org.apache.ignite.internal.processors.task.monitor;
 
 /**
- * Zookeeper IP Finder tests.
+ * Task status.
  */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
+public enum ComputeTaskStatusEnum {
+    /** Task is in progress. */
+    RUNNING,
 
+    /** Task is finished. */
+    FINISHED,
+
+    /** Task has failed. */
+    FAILED;
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusSnapshot.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusSnapshot.java
new file mode 100644
index 0000000..995e3b2
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/task/monitor/ComputeTaskStatusSnapshot.java
@@ -0,0 +1,89 @@
+/*
+ * 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.task.monitor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Snapshot of the task status.
+ */
+public interface ComputeTaskStatusSnapshot {
+    /**
+     * @return Session ID of the task being executed.
+     */
+    IgniteUuid sessionId();
+
+    /**
+     * @return Task name of the task this session belongs to.
+     */
+    String taskName();
+
+    /**
+     * @return ID of the node on which task execution originated.
+     */
+    UUID originatingNodeId();
+
+    /**
+     * @return Start of computation time for the task.
+     */
+    long startTime();
+
+    /**
+     * @return End of computation time for the task.
+     */
+    long endTime();
+
+    /**
+     * @return Nodes IDs on which the task jobs will execute.
+     */
+    List<UUID> jobNodes();
+
+    /**
+     * @return All session attributes.
+     */
+    Map<?, ?> attributes();
+
+    /**
+     * @return Status of the task.
+     */
+    ComputeTaskStatusEnum status();
+
+    /**
+     * @return Reason for the failure of the task.
+     */
+    @Nullable Throwable failReason();
+
+    /**
+     * @return {@code true} if change of task attributes is available.
+     */
+    boolean fullSupport();
+
+    /**
+     * @return User who created the task, {@code null} if security is not available.
+     */
+    @Nullable Object createBy();
+
+    /**
+     * @return {@code True} if task is internal.
+     */
+    boolean internal();
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
index f3c0d8b..7d990f9 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
@@ -4245,9 +4245,12 @@
             return;
 
         try {
-            // Avoid tls 1.3 incompatibility https://bugs.openjdk.java.net/browse/JDK-8208526
-            sock.shutdownOutput();
-            sock.shutdownInput();
+            // Closing output and input first to avoid tls 1.3 incompatibility
+            // https://bugs.openjdk.java.net/browse/JDK-8208526
+            if (!sock.isOutputShutdown())
+                sock.shutdownOutput();
+            if (!sock.isInputShutdown())
+                sock.shutdownInput();
         }
         catch (ClosedChannelException | SocketException ex) {
             LT.warn(log, "Failed to shutdown socket", ex);
@@ -5537,6 +5540,24 @@
     }
 
     /**
+     * Writes long array to output stream.
+     *
+     * @param out Output stream to write to.
+     * @param arr Array to write.
+     * @throws IOException If write failed.
+     */
+    public static void writeLongArray(DataOutput out, @Nullable long[] arr) throws IOException {
+        if (arr == null)
+            out.writeInt(-1);
+        else {
+            out.writeInt(arr.length);
+
+            for (long b : arr)
+                out.writeLong(b);
+        }
+    }
+
+    /**
      * Reads boolean array from input stream accounting for <tt>null</tt> values.
      *
      * @param in Stream to read from.
@@ -5579,6 +5600,27 @@
     }
 
     /**
+     * Reads long array from input stream.
+     *
+     * @param in Stream to read from.
+     * @return Read long array, possibly <tt>null</tt>.
+     * @throws IOException If read failed.
+     */
+    @Nullable public static long[] readLongArray(DataInput in) throws IOException {
+        int len = in.readInt();
+
+        if (len == -1)
+            return null; // Value "-1" indicates null.
+
+        long[] res = new long[len];
+
+        for (int i = 0; i < len; i++)
+            res[i] = in.readLong();
+
+        return res;
+    }
+
+    /**
      * Calculates hash code for the given byte buffers contents. Compatible with {@link Arrays#hashCode(byte[])}
      * with the same content. Does not change buffers positions.
      *
@@ -6114,6 +6156,25 @@
     }
 
     /**
+     * Provides all interfaces of {@code cls} including inherited ones. Excludes duplicated ones in case of multiple
+     * inheritance.
+     *
+     * @param cls Class to search for interfaces.
+     * @return Collection of interfaces of {@code cls}.
+     */
+    public static Collection<Class<?>> allInterfaces(Class<?> cls) {
+        Set<Class<?>> interfaces = new HashSet<>();
+
+        while (cls != null) {
+            interfaces.addAll(Arrays.asList(cls.getInterfaces()));
+
+            cls = cls.getSuperclass();
+        }
+
+        return interfaces;
+    }
+
+    /**
      * Gets simple class name taking care of empty names.
      *
      * @param cls Class to get the name for.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/SingleNodeMessage.java b/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/SingleNodeMessage.java
index dab288e..6d46e60 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/SingleNodeMessage.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/SingleNodeMessage.java
@@ -135,19 +135,23 @@
                 reader.incrementState();
 
             case 2:
-                resp = U.fromBytes(reader.readByteArray("data"));
+                byte[] dataBytes = reader.readByteArray("data");
 
                 if (!reader.isLastRead())
                     return false;
 
+                resp = U.fromBytes(dataBytes);
+
                 reader.incrementState();
 
             case 3:
-                err = U.fromBytes(reader.readByteArray("err"));
+                byte[] errBytes = reader.readByteArray("err");
 
                 if (!reader.isLastRead())
                     return false;
 
+                err = U.fromBytes(errBytes);
+
                 reader.incrementState();
         }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/gridify/GridifyUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/gridify/GridifyUtils.java
index 43d0243..1ccd023 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/gridify/GridifyUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/gridify/GridifyUtils.java
@@ -40,6 +40,7 @@
 import org.apache.ignite.compute.gridify.GridifyInput;
 import org.apache.ignite.compute.gridify.GridifySetToSet;
 import org.apache.ignite.compute.gridify.GridifySetToValue;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.SB;
 import org.jetbrains.annotations.Nullable;
 
@@ -324,7 +325,7 @@
 
             return res;
         }
-        else if (arg != null && arg.getClass().isArray()) {
+        else if (F.isArray(arg)) {
             Collection res = new ArrayList();
 
             for (int i = 0; i < Array.getLength(arg); i++)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java b/modules/core/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java
index fb29bb0..dc26ca2 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java
@@ -53,6 +53,7 @@
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.util.F0;
 import org.apache.ignite.internal.util.GridConcurrentHashSet;
 import org.apache.ignite.internal.util.GridEmptyIterator;
@@ -3379,4 +3380,244 @@
             }
         };
     }
+
+    /**
+     * @param val Value to check.
+     * @return {@code True} if not null and array.
+     */
+    public static boolean isArray(Object val) {
+        return val != null && val.getClass().isArray();
+    }
+
+    /**
+     * Check for arrays equality.
+     *
+     * @param a1 Value 1.
+     * @param a2 Value 2.
+     * @return {@code True} if arrays equal.
+     */
+    public static boolean arrayEq(Object a1, Object a2) {
+        if (a1 == a2)
+            return true;
+
+        if (a1 == null || a2 == null)
+            return a1 != null || a2 != null;
+
+        if (a1.getClass() != a2.getClass())
+            return false;
+
+        if (a1 instanceof byte[])
+            return Arrays.equals((byte[])a1, (byte[])a2);
+        else if (a1 instanceof boolean[])
+            return Arrays.equals((boolean[])a1, (boolean[])a2);
+        else if (a1 instanceof short[])
+            return Arrays.equals((short[])a1, (short[])a2);
+        else if (a1 instanceof char[])
+            return Arrays.equals((char[])a1, (char[])a2);
+        else if (a1 instanceof int[])
+            return Arrays.equals((int[])a1, (int[])a2);
+        else if (a1 instanceof long[])
+            return Arrays.equals((long[])a1, (long[])a2);
+        else if (a1 instanceof float[])
+            return Arrays.equals((float[])a1, (float[])a2);
+        else if (a1 instanceof double[])
+            return Arrays.equals((double[])a1, (double[])a2);
+        else if (a1 instanceof BinaryArray)
+            return a1.equals(a2);
+
+        return Arrays.deepEquals((Object[])a1, (Object[])a2);
+    }
+
+    /**
+     * Compare arrays.
+     *
+     * @param a1 Value 1.
+     * @param a2 Value 2.
+     * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to,
+     * or greater than the second.
+     */
+    public static int compareArrays(Object a1, Object a2) {
+        if (a1 == a2)
+            return 0;
+
+        if (a1 == null || a2 == null)
+            return a1 != null ? 1 : -1;
+
+        if (a1.getClass() != a2.getClass()) {
+            throw new IllegalArgumentException(
+                "Can't compare arrays of different types[a1=" + a1.getClass() + ",a2=" + a2.getClass() + ']'
+            );
+        }
+
+        if (a1 instanceof byte[])
+            return compareArrays((byte[])a1, (byte[])a2);
+        else if (a1 instanceof boolean[])
+            return compareArrays((boolean[])a1, (boolean[])a2);
+        else if (a1 instanceof short[])
+            return compareArrays((short[])a1, (short[])a2);
+        else if (a1 instanceof char[])
+            return compareArrays((char[])a1, (char[])a2);
+        else if (a1 instanceof int[])
+            return compareArrays((int[])a1, (int[])a2);
+        else if (a1 instanceof long[])
+            return compareArrays((long[])a1, (long[])a2);
+        else if (a1 instanceof float[])
+            return compareArrays((float[])a1, (float[])a2);
+        else if (a1 instanceof double[])
+            return compareArrays((double[])a1, (double[])a2);
+        else if (a1 instanceof Object[])
+            return compareArrays((Object[])a1, (Object[])a2);
+
+        throw new IllegalStateException("Unknown array type " + a1.getClass());
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(Object[] a1, Object[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] == null || a2[i] == null) {
+                if (a1[i] != null || a2[i] != null)
+                    return a1[i] != null ? 1 : -1;
+
+                continue;
+            }
+
+            if (F.isArray(a1[i]) && F.isArray(a2[i])) {
+                int res = compareArrays(a1[i], a2[i]);
+
+                if (res != 0)
+                    return res;
+            }
+
+            return ((Comparable)a1[i]).compareTo(a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(byte[] a1, byte[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Byte.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(boolean[] a1, boolean[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Boolean.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(short[] a1, short[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Short.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(char[] a1, char[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Character.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(int[] a1, int[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Integer.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(long[] a1, long[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+
+        for (int i = 0; i < l; i++) {
+            if (a1[i] != a2[i])
+                return Long.compare(a1[i], a2[i]);
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(float[] a1, float[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+        int res;
+
+        for (int i = 0; i < l; i++) {
+            if ((res = Float.compare(a1[i], a2[i])) != 0)
+                return res;
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
+
+    /** Compare arrays. */
+    public static int compareArrays(double[] a1, double[] a2) {
+        if (a1 == a2)
+            return 0;
+
+        int l = Math.min(a1.length, a2.length);
+        int res;
+
+        for (int i = 0; i < l; i++) {
+            if ((res = Double.compare(a1[i], a2[i])) != 0)
+                return res;
+        }
+
+        return Integer.compare(a1.length, a2.length);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslHandler.java
index bde354d..33000c7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslHandler.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslHandler.java
@@ -331,7 +331,8 @@
 
         if (!handshakeFinished)
             handshake();
-        else
+
+        if (inNetBuf.hasRemaining())
             unwrapData();
 
         if (isInboundDone()) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelper.java b/modules/core/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelper.java
index 1847411..cddef65 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelper.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelper.java
@@ -95,11 +95,13 @@
      *
      * @param cfgUrl Configuration file path or URL. This cannot be {@code null}.
      * @param beanClasses Beans classes.
-     * @return Bean class -> loaded bean instance map, if configuration does not contain bean with required type the
-     *       map value is {@code null}.
+     * @return Tuple containing all loaded beans and Spring context used to load them.
      * @throws IgniteCheckedException If failed to load configuration.
      */
-    public Map<Class<?>, Object> loadBeans(URL cfgUrl, Class<?>... beanClasses) throws IgniteCheckedException;
+    public IgniteBiTuple<Map<Class<?>, Collection>, ? extends GridSpringResourceContext> loadBeans(
+        URL cfgUrl,
+        Class<?>... beanClasses
+    ) throws IgniteCheckedException;
 
     /**
      * Loads bean instance by name.
@@ -112,18 +114,6 @@
     public <T> T loadBean(URL url, String beanName) throws IgniteCheckedException;
 
     /**
-     * Loads bean instances that match the given types from given configuration input stream.
-     *
-     * @param cfgStream Input stream containing Spring XML configuration. This cannot be {@code null}.
-     * @param beanClasses Beans classes.
-     * @return Bean class -> loaded bean instance map, if configuration does not contain bean with required type the
-     *       map value is {@code null}.
-     * @throws IgniteCheckedException If failed to load configuration.
-     */
-    public Map<Class<?>, Object> loadBeans(InputStream cfgStream, Class<?>... beanClasses)
-        throws IgniteCheckedException;
-
-    /**
      * Loads bean instance by name.
      *
      * @param stream Input stream containing Spring XML configuration.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTask.java
index 1380bee..0948c30 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTask.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTask.java
@@ -25,13 +25,14 @@
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.events.CacheConsistencyViolationEvent;
 import org.apache.ignite.internal.processors.cache.CacheGroupContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtLocalPartition;
-import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteConsistencyViolationException;
+import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteIrreparableConsistencyViolationException;
 import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
 import org.apache.ignite.internal.util.GridConcurrentHashSet;
 import org.apache.ignite.internal.util.lang.GridCursor;
@@ -87,6 +88,8 @@
         /** {@inheritDoc} */
         @Override protected String run(VisorConsistencyRepairTaskArg arg) throws IgniteException {
             String cacheName = arg.cacheName();
+            ReadRepairStrategy strategy = arg.strategy();
+
             int p = arg.part();
             int batchSize = 1024;
             int statusDelay = 60_000; // Every minute.
@@ -111,7 +114,8 @@
             if (part == null)
                 return null; // Partition does not belong to the node.
 
-            log.info("Consistency check started [grp=" + grpCtx.cacheOrGroupName() + ", part=" + p + "]");
+            log.info("Consistency check started " +
+                "[grp=" + grpCtx.cacheOrGroupName() + ", part=" + p + ", strategy=" + strategy + "]");
 
             VisorConsistencyStatusTask.MAP.put(arg, "0/" + part.fullSize());
 
@@ -130,7 +134,7 @@
 
                     GridCursor<? extends CacheDataRow> cursor = grpCtx.offheap().dataStore(part).cursor(cctx.cacheId());
 
-                    IgniteCache<Object, Object> cache = ignite.cache(cacheName).withKeepBinary().withReadRepair();
+                    IgniteCache<Object, Object> cache = ignite.cache(cacheName).withKeepBinary().withReadRepair(strategy);
 
                     do {
                         keys.clear();
@@ -152,7 +156,7 @@
                             cache.getAll(keys); // Repair.
                         }
                         catch (CacheException e) {
-                            if (!(e.getCause() instanceof IgniteConsistencyViolationException) // Found but not fixed.
+                            if (!(e.getCause() instanceof IgniteIrreparableConsistencyViolationException) // Found but not fixed.
                                 && !isCancelled())
                                 throw new IgniteException("Read repair attempt failed.", e);
                         }
@@ -200,7 +204,7 @@
             StringBuilder sb = new StringBuilder();
 
             for (CacheConsistencyViolationEvent evt : evts) {
-                for (Map.Entry<Object, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> entry : evt.getEntries().entrySet()) {
+                for (Map.Entry<?, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> entry : evt.getEntries().entrySet()) {
                     Object key = entry.getKey();
 
                     if (cctx.affinity().partition(key) != part)
@@ -209,7 +213,11 @@
                     found++;
 
                     sb.append("Key: ").append(key)
-                        .append(" (Cache: ").append(evt.getCacheName()).append(")").append("\n");
+                        .append(" (cache: ").append(evt.getCacheName())
+                        .append(", strategy: ").append(evt.getStrategy()).append(")").append("\n");
+
+                    if (evt.getFixedEntries().containsKey(key))
+                        sb.append(" Fixed: ").append(evt.getFixedEntries().get(key)).append("\n");
 
                     for (Map.Entry<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> mapping : entry.getValue().entrySet()) {
                         ClusterNode node = mapping.getKey();
@@ -217,14 +225,19 @@
 
                         sb.append("  Node: ").append(node).append("\n")
                             .append("    Value: ").append(info.getValue()).append("\n")
-                            .append("    Version: ").append(info.getVersion()).append("\n")
-                            .append("    Other cluster version: ").append(info.getVersion().otherClusterVersion()).append("\n")
-                            .append("    On primary: ").append(info.isPrimary()).append("\n")
-                            .append("    Considered as a correct value: ").append(info.isCorrect()).append("\n");
+                            .append("    Version: ").append(info.getVersion()).append("\n");
+
+                        if (info.getVersion() != null)
+                            sb.append("    Other cluster version: ").append(info.getVersion().otherClusterVersion()).append("\n");
+
+                        sb.append("    On primary: ").append(info.isPrimary()).append("\n");
 
                         if (info.isCorrect())
-                            fixed++;
+                            sb.append("    Considered as a CORRECT value!").append("\n");
                     }
+
+                    if (evt.getFixedEntries().containsKey(key))
+                        fixed++;
                 }
             }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTaskArg.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTaskArg.java
index 134cda2..50c8b3f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTaskArg.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/consistency/VisorConsistencyRepairTaskArg.java
@@ -21,6 +21,7 @@
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.util.Objects;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.internal.dto.IgniteDataTransferObject;
 import org.apache.ignite.internal.util.typedef.internal.U;
 
@@ -37,6 +38,9 @@
     /** Partition. */
     private int part;
 
+    /** Strategy. */
+    ReadRepairStrategy strategy;
+
     /**
      * Default constructor.
      */
@@ -47,15 +51,17 @@
      * @param cacheName Cache name.
      * @param part Part.
      */
-    public VisorConsistencyRepairTaskArg(String cacheName, int part) {
+    public VisorConsistencyRepairTaskArg(String cacheName, int part, ReadRepairStrategy strategy) {
         this.cacheName = cacheName;
         this.part = part;
+        this.strategy = strategy;
     }
 
     /** {@inheritDoc} */
     @Override protected void writeExternalData(ObjectOutput out) throws IOException {
         U.writeString(out, cacheName);
         out.writeInt(part);
+        U.writeEnum(out, strategy);
     }
 
     /** {@inheritDoc} */
@@ -63,6 +69,7 @@
         ObjectInput in) throws IOException, ClassNotFoundException {
         cacheName = U.readString(in);
         part = in.readInt();
+        strategy = U.readEnum(in, ReadRepairStrategy.class);
     }
 
     /**
@@ -79,6 +86,13 @@
         return part;
     }
 
+    /**
+     *
+     */
+    public ReadRepairStrategy strategy() {
+        return strategy;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorDataStorageConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorDataStorageConfiguration.java
index 76d2d94..8dad47c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorDataStorageConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorDataStorageConfiguration.java
@@ -146,8 +146,8 @@
     public VisorDataStorageConfiguration(DataStorageConfiguration cfg) {
         assert cfg != null;
 
-        sysRegionInitSize = cfg.getSystemRegionInitialSize();
-        sysRegionMaxSize = cfg.getSystemRegionMaxSize();
+        sysRegionInitSize = cfg.getSystemDataRegionConfiguration().getInitialSize();
+        sysRegionMaxSize = cfg.getSystemDataRegionConfiguration().getMaxSize();
         pageSize = cfg.getPageSize();
         concLvl = cfg.getConcurrencyLevel();
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorMemoryConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorMemoryConfiguration.java
index 01a2fda..18835a2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorMemoryConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/node/VisorMemoryConfiguration.java
@@ -72,8 +72,8 @@
     public VisorMemoryConfiguration(DataStorageConfiguration memCfg) {
         assert memCfg != null;
 
-        sysCacheInitSize = memCfg.getSystemRegionInitialSize();
-        sysCacheMaxSize = memCfg.getSystemRegionMaxSize();
+        sysCacheInitSize = memCfg.getSystemDataRegionConfiguration().getInitialSize();
+        sysCacheMaxSize = memCfg.getSystemDataRegionConfiguration().getMaxSize();
         pageSize = memCfg.getPageSize();
         concLvl = memCfg.getConcurrencyLevel();
 //        dfltMemPlcName = memCfg.getDefaultDataRegionName();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/snapshot/VisorSnapshotRestoreTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/snapshot/VisorSnapshotRestoreTask.java
index 8f4ab22..83c7497 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/snapshot/VisorSnapshotRestoreTask.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/snapshot/VisorSnapshotRestoreTask.java
@@ -93,7 +93,7 @@
             boolean stopped = ignite.snapshot().cancelSnapshotRestore(arg.snapshotName()).get();
 
             return "Snapshot cache group restore operation " +
-                (stopped ? "canceled" : "is not in progress") + " [snapshot=" + arg.snapshotName() + ']';
+                (stopped ? "canceled" : "is NOT running") + " [snapshot=" + arg.snapshotName() + ']';
         }
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/systemview/VisorSystemViewTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/systemview/VisorSystemViewTask.java
index 9ba2658..6f6e015 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/systemview/VisorSystemViewTask.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/systemview/VisorSystemViewTask.java
@@ -98,7 +98,7 @@
                 List<Serializable> attrVals = new ArrayList<>();
 
                 ((SystemView<Object>)sysView).walker().visitAll(row, new AttributeWithValueVisitor() {
-                    @Override public <T> void accept(int idx, String name, Class<T> clazz, T val) {
+                    @Override public <T> void accept(int idx, String name, Class<T> clazz, @Nullable T val) {
                         if (clazz.isEnum())
                             attrVals.add(val == null ? null : ((Enum<?>)val).name());
                         else if (Class.class.isAssignableFrom(clazz))
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/tx/TxVerboseId.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/tx/TxVerboseId.java
index 0bb25ac..afd4547 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/tx/TxVerboseId.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/tx/TxVerboseId.java
@@ -88,7 +88,7 @@
                 "either UUID or GridCacheVersion text representation: " + text);
         }
 
-        assert m.groupCount() == 3 : "Unexpected group count [cnt=" + m.groupCount() + ", pattern=" + regexPtrn + ']';
+        assert m.groupCount() == 4 : "Unexpected group count [cnt=" + m.groupCount() + ", pattern=" + regexPtrn + ']';
 
         try {
             return new TxVerboseId(null, new GridCacheVersion(
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/main/java/org/apache/ignite/mem/MemoryAllocator.java
similarity index 66%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/main/java/org/apache/ignite/mem/MemoryAllocator.java
index a244658..0d68739 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/main/java/org/apache/ignite/mem/MemoryAllocator.java
@@ -14,17 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package org.apache.ignite.internal.processors.cache.consistency;
-
-import org.apache.ignite.cache.CacheMode;
+package org.apache.ignite.mem;
 
 /**
- *
+ * Base interface for offheap memory allocator.
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
-    /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
-    }
+public interface MemoryAllocator {
+    /**
+     * @param size Size of allocated memory.
+     *
+     * @return Pointer to memory or {@code 0} if failed.
+     */
+    public long allocateMemory(long size);
+
+    /**
+     * Deallocates memory.
+     *
+     * @param addr Address of memory.
+     */
+    public void freeMemory(long addr);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/mxbean/SnapshotMXBean.java b/modules/core/src/main/java/org/apache/ignite/mxbean/SnapshotMXBean.java
index e6b42583..fcb7a399 100644
--- a/modules/core/src/main/java/org/apache/ignite/mxbean/SnapshotMXBean.java
+++ b/modules/core/src/main/java/org/apache/ignite/mxbean/SnapshotMXBean.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.mxbean;
 
+import java.util.Collection;
 import org.apache.ignite.IgniteSnapshot;
 
 /**
@@ -40,4 +41,28 @@
      */
     @MXBeanDescription("Cancel started cluster-wide snapshot on the node initiator.")
     public void cancelSnapshot(@MXBeanParameter(name = "snpName", description = "Snapshot name.") String snpName);
+
+    /**
+     * Restore cluster-wide snapshot.
+     *
+     * @param name Snapshot name.
+     * @param cacheGroupNames Optional comma-separated list of cache group names.
+     * @see IgniteSnapshot#restoreSnapshot(String, Collection)
+     */
+    @MXBeanDescription("Restore cluster-wide snapshot.")
+    public void restoreSnapshot(
+        @MXBeanParameter(name = "snpName", description = "Snapshot name.")
+            String name,
+        @MXBeanParameter(name = "cacheGroupNames", description = "Optional comma-separated list of cache group names.")
+            String cacheGroupNames
+    );
+
+    /**
+     * Cancel previously started snapshot restore operation.
+     *
+     * @param name Snapshot name.
+     * @see IgniteSnapshot#cancelSnapshotRestore(String)
+     */
+    @MXBeanDescription("Cancel previously started snapshot restore operation.")
+    public void cancelSnapshotRestore(@MXBeanParameter(name = "snpName", description = "Snapshot name.") String name);
 }
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/core/src/main/java/org/apache/ignite/platform/PlatformType.java
similarity index 74%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/core/src/main/java/org/apache/ignite/platform/PlatformType.java
index a5b6d8f..0751db2 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/core/src/main/java/org/apache/ignite/platform/PlatformType.java
@@ -15,18 +15,15 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+package org.apache.ignite.platform;
 
 /**
- * Zookeeper IP Finder tests.
+ * Interop platform type.
  */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
+public enum PlatformType {
+    /** Java platform. */
+    JAVA,
 
+    /** .NET platform. */
+    DOTNET
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/plugin/CacheTopologyValidatorProvider.java b/modules/core/src/main/java/org/apache/ignite/plugin/CacheTopologyValidatorProvider.java
new file mode 100644
index 0000000..c9361b7
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/plugin/CacheTopologyValidatorProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.plugin;
+
+import org.apache.ignite.configuration.TopologyValidator;
+
+/**
+ * The {@link CacheTopologyValidatorProvider} is used to pass an implementation of {@link TopologyValidator}s for a specific
+ * cache through Ignite plugin extensions mechanism. Each cache, on startup, iterates over all registered
+ * {@link CacheTopologyValidatorProvider}s and tries to obtain the instance of {@link TopologyValidator} by
+ * cache name. All obtained {@link TopologyValidator}s are saved in the cache context and successively called during
+ * topology validation process. The topology validation is passed if all {@link TopologyValidator}s consider that
+ * the current topology is valid.
+ *
+ * @see TopologyValidator
+ * @see Extension
+ * @see PluginProvider#initExtensions(PluginContext, ExtensionRegistry)
+ */
+public interface CacheTopologyValidatorProvider extends Extension {
+    /**
+     * Provides instance of {@link TopologyValidator} for the cache with specified name.
+     *
+     * @param cacheName Name of the cache or cache group.
+     * @return Instance of topology validator for the cache with specified name.
+     */
+    public TopologyValidator topologyValidator(String cacheName);
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
index 3830edc..8eae10b 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.services;
 
-import java.io.Externalizable;
 import org.apache.ignite.lang.IgniteExperimental;
 
 /**
@@ -58,7 +57,7 @@
  * @see ServiceCallContextBuilder
  */
 @IgniteExperimental
-public interface ServiceCallContext extends Externalizable {
+public interface ServiceCallContext {
     /**
      * Create a context builder.
      *
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
index a517197..ad2c694 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
@@ -17,8 +17,11 @@
 
 package org.apache.ignite.services;
 
+import java.io.Externalizable;
 import java.io.Serializable;
+import org.apache.ignite.IgniteServices;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.processors.service.IgniteServiceProcessor;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lang.IgnitePredicate;
@@ -77,6 +80,9 @@
     @GridToStringExclude
     protected IgnitePredicate<ClusterNode> nodeFilter;
 
+    /** Enables or disables service statistics. */
+    protected boolean isStatisticsEnabled;
+
     /**
      * Gets service name.
      * <p>
@@ -256,6 +262,36 @@
         return this;
     }
 
+    /**
+     * Enables or disables statistics for the service. If enabled, durations of the service's methods invocations are
+     * measured (in milliseconds) and stored in histograms of metric registry
+     * {@link IgniteServiceProcessor#SERVICE_METRIC_REGISTRY} by service name.
+     * <p>
+     * <b>NOTE:</b> Statistics are collected only with service proxies obtaining by methods like
+     * {@link IgniteServices#serviceProxy(String, Class, boolean)} and won't work for direct reference of local
+     * services which you can get by, for example, {@link IgniteServices#service(String)}.
+     * <p>
+     * <b>NOTE:</b> Statistics are collected only for all service's interfaces except {@link Service} and
+     * {@link Externalizable} if implemented. Statistics are not collected for methods not declared in any interface.
+     *
+     * @param enabled If {@code true}, enables service statistics. Disables otherwise.
+     * @return {@code this} for chaining.
+     */
+    public ServiceConfiguration setStatisticsEnabled(boolean enabled) {
+        isStatisticsEnabled = enabled;
+
+        return this;
+    }
+
+    /**
+     * Tells wheter statistics for this service is enabled.
+     *
+     * @return {@code True}, if statistics for this service will be enabled. {@code False} otherwise.
+     */
+    public boolean isStatisticsEnabled() {
+        return isStatisticsEnabled;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (!equalsIgnoreNodeFilter(o))
diff --git a/modules/core/src/main/java/org/apache/ignite/spi/collision/priorityqueue/PriorityQueueCollisionSpi.java b/modules/core/src/main/java/org/apache/ignite/spi/collision/priorityqueue/PriorityQueueCollisionSpi.java
index d4247e4..a40e3c9 100644
--- a/modules/core/src/main/java/org/apache/ignite/spi/collision/priorityqueue/PriorityQueueCollisionSpi.java
+++ b/modules/core/src/main/java/org/apache/ignite/spi/collision/priorityqueue/PriorityQueueCollisionSpi.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.spi.collision.priorityqueue;
 
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -27,7 +26,7 @@
 import java.util.Map;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.compute.ComputeJobContext;
-import org.apache.ignite.compute.ComputeTaskSession;
+import org.apache.ignite.internal.GridTaskSessionInternal;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.internal.util.typedef.internal.LT;
@@ -239,7 +238,8 @@
     private volatile boolean preventStarvation = DFLT_PREVENT_STARVATION_ENABLED;
 
     /** Cached priority comparator instance. */
-    private Comparator<GridCollisionJobContextWrapper> priComp;
+    private final Comparator<GridCollisionJobContextWrapper> priComp =
+        Comparator.comparing(w -> getJobPriority(w.ctx), Comparator.reverseOrder());
 
     /** */
     @LoggerResource
@@ -530,7 +530,7 @@
                 }
             }
             else {
-                Collections.sort(waitSnap, priorityComparator());
+                waitSnap.sort(priComp);
                 waitSnapSorted = true;
 
                 if (preventStarvation)
@@ -551,7 +551,7 @@
             int skip = waitSnap.size() - waitSize;
 
             if (!waitSnapSorted)
-                Collections.sort(waitSnap, priorityComparator());
+                waitSnap.sort(priComp);
 
             int i = 0;
 
@@ -604,7 +604,8 @@
 
     /**
      * Gets job priority. At first tries to get from job context. If job context has no priority,
-     * then tries to get from task session. If task session has no priority default one will be used.
+     * then tries to get from task session. If task session does not support attributes or there
+     * is no priority in them, then the default priority is used.
      *
      * @param ctx Collision job context.
      * @return Job priority.
@@ -612,43 +613,49 @@
     private int getJobPriority(CollisionJobContext ctx) {
         assert ctx != null;
 
-        Integer p = null;
+        Integer pri = null;
 
-        ComputeJobContext jctx = ctx.getJobContext();
+        ComputeJobContext jobCtx = ctx.getJobContext();
 
         try {
-            p = (Integer)jctx.getAttribute(jobPriAttrKey);
+            pri = jobCtx.getAttribute(jobPriAttrKey);
         }
         catch (ClassCastException e) {
             LT.error(log, e, "Type of job context priority attribute '" + jobPriAttrKey +
-                "' is not java.lang.Integer [type=" + jctx.getAttribute(jobPriAttrKey).getClass() + ']');
+                "' is not java.lang.Integer [type=" + jobCtx.getAttribute(jobPriAttrKey).getClass() + ']');
         }
 
-        if (p == null) {
-            ComputeTaskSession ses = ctx.getTaskSession();
+        if (pri == null) {
+            GridTaskSessionInternal taskSes = (GridTaskSessionInternal)ctx.getTaskSession();
 
-            try {
-                p = (Integer)ses.getAttribute(taskPriAttrKey);
-            }
-            catch (ClassCastException e) {
-                LT.error(log, e, "Type of task session priority attribute '" + taskPriAttrKey +
-                    "' is not java.lang.Integer [type=" + ses.getAttribute(taskPriAttrKey).getClass() + ']');
-            }
+            if (!taskSes.isFullSupport()) {
+                if (log.isDebugEnabled())
+                    log.debug("Task does not support session attributes (will use default priority): " + dfltPri);
 
-            if (p == null) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Failed get priority from job context attribute '" + jobPriAttrKey +
-                        "' and task session attribute '" + taskPriAttrKey + "' (will use default priority): " +
-                        dfltPri);
+                pri = dfltPri;
+            }
+            else {
+                try {
+                    pri = taskSes.getAttribute(taskPriAttrKey);
+                }
+                catch (ClassCastException e) {
+                    LT.error(log, e, "Type of task session priority attribute '" + taskPriAttrKey +
+                        "' is not java.lang.Integer [type=" + taskSes.getAttribute(taskPriAttrKey).getClass() + ']');
                 }
 
-                p = dfltPri;
+                if (pri == null) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("Failed get priority from job context attribute '" + jobPriAttrKey +
+                            "' and task session attribute '" + taskPriAttrKey + "' (will use default priority): " +
+                            dfltPri);
+                    }
+
+                    pri = dfltPri;
+                }
             }
         }
 
-        assert p != null;
-
-        return p;
+        return pri;
     }
 
     /** {@inheritDoc} */
@@ -656,19 +663,6 @@
         return Collections.singletonList(createSpiAttributeName(PRIORITY_ATTRIBUTE_KEY));
     }
 
-    /**
-     * Returns (possibly shared) comparator fo sorting GridCollisionJobContextWrapper
-     * by priority.
-     *
-     * @return Comparator for priority sorting.
-     */
-    private Comparator<GridCollisionJobContextWrapper> priorityComparator() {
-        if (priComp == null)
-            priComp = new PriorityGridCollisionJobContextComparator();
-
-        return priComp;
-    }
-
     /** {@inheritDoc} */
     @Override public PriorityQueueCollisionSpi setName(String name) {
         super.setName(name);
@@ -682,22 +676,6 @@
     }
 
     /**
-     * Comparator for by priority comparison of collision contexts.
-     */
-    private class PriorityGridCollisionJobContextComparator implements Comparator<GridCollisionJobContextWrapper>, Serializable {
-        /** */
-        private static final long serialVersionUID = 0L;
-
-        /** {@inheritDoc} */
-        @Override public int compare(GridCollisionJobContextWrapper o1, GridCollisionJobContextWrapper o2) {
-            int p1 = getJobPriority(o1.getContext());
-            int p2 = getJobPriority(o2.getContext());
-
-            return p1 < p2 ? 1 : p1 == p2 ? 0 : -1;
-        }
-    }
-
-    /**
      * Wrapper class to keep original collision context position.
      */
     private static class GridCollisionJobContextWrapper {
diff --git a/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/CacheView.java b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/CacheView.java
index e2c0932..72c663f 100644
--- a/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/CacheView.java
+++ b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/CacheView.java
@@ -27,10 +27,13 @@
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.NearCacheConfiguration;
 import org.apache.ignite.configuration.TopologyValidator;
+import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.managers.systemview.walker.Order;
 import org.apache.ignite.internal.processors.cache.CacheGroupDescriptor;
 import org.apache.ignite.internal.processors.cache.CacheType;
 import org.apache.ignite.internal.processors.cache.DynamicCacheDescriptor;
+import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
+import org.apache.ignite.internal.processors.cache.version.CacheVersionConflictResolver;
 
 import static org.apache.ignite.internal.util.IgniteUtils.toStringSafe;
 
@@ -39,13 +42,18 @@
  */
 public class CacheView {
     /** Cache descriptor. */
-    private DynamicCacheDescriptor cache;
+    private final DynamicCacheDescriptor cache;
+
+    /** Kernal context. */
+    private final GridKernalContext ctx;
 
     /**
      * @param cache Cache descriptor.
+     * @param ctx Kernal context.
      */
-    public CacheView(DynamicCacheDescriptor cache) {
+    public CacheView(DynamicCacheDescriptor cache, GridKernalContext ctx) {
         this.cache = cache;
+        this.ctx = ctx;
     }
 
     /** @see DynamicCacheDescriptor#groupId() */
@@ -370,4 +378,14 @@
     public String dataRegionName() {
         return cache.cacheConfiguration().getDataRegionName();
     }
+
+    /** @see CacheVersionConflictResolver */
+    public String conflictResolver() {
+        IgniteInternalCache<Object, Object> cache = ctx.cache().cache(this.cache.cacheName());
+
+        if (cache == null || !cache.context().conflictNeedResolve())
+            return null;
+
+        return toStringSafe(cache.context().conflictResolver());
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SnapshotView.java b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SnapshotView.java
new file mode 100644
index 0000000..3b17ca2
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SnapshotView.java
@@ -0,0 +1,93 @@
+/*
+ * 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.spi.systemview.view;
+
+import org.apache.ignite.internal.managers.systemview.walker.Order;
+
+/**
+ * Snapshot representation for a {@link SystemView}.
+ */
+public class SnapshotView {
+    /** Snapshot system view name. */
+    public static final String SNAPSHOT_SYS_VIEW = "snapshot";
+
+    /** Snapshot system view description. */
+    public static final String SNAPSHOT_SYS_VIEW_DESC = "Snapshot";
+
+    /** Snapshot name. */
+    private final String name;
+
+    /** Node consistent ID. */
+    private final String consistentId;
+
+    /** Baseline nodes affected by the snapshot. */
+    private final String baselineNodes;
+
+    /** Cache group names that were included in the snapshot. */
+    private final String cacheGrps;
+
+    /**
+     * @param name Snapshot name.
+     * @param consistentId Node consistent ID.
+     * @param baselineNodes Baseline nodes affected by the snapshot.
+     * @param cacheGrps Cache group names that were included in the snapshot.
+     */
+    public SnapshotView(
+        String name,
+        String consistentId,
+        String baselineNodes,
+        String cacheGrps
+    ) {
+        this.name = name;
+        this.consistentId = consistentId;
+        this.baselineNodes = baselineNodes;
+        this.cacheGrps = cacheGrps;
+    }
+
+    /**
+     * @return Snapshot name.
+     */
+    @Order
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Node consistent ID.
+     */
+    @Order(1)
+    public String consistentId() {
+        return consistentId;
+    }
+
+    /**
+     * @return Baseline nodes affected by the snapshot.
+     */
+    @Order(2)
+    public String baselineNodes() {
+        return baselineNodes;
+    }
+
+    /**
+     * @return Cache group names that were included in the snapshot.
+     */
+    @Order(3)
+    public String cacheGroups() {
+        return cacheGrps;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SystemViewRowAttributeWalker.java b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SystemViewRowAttributeWalker.java
index a902442..3bfb3e4 100644
--- a/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SystemViewRowAttributeWalker.java
+++ b/modules/core/src/main/java/org/apache/ignite/spi/systemview/view/SystemViewRowAttributeWalker.java
@@ -19,6 +19,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Utility class for quick iteration over row properties.
@@ -73,7 +74,7 @@
          * @param val Value.
          * @param <T> Value type.
          */
-        public <T> void accept(int idx, String name, Class<T> clazz, T val);
+        public <T> void accept(int idx, String name, Class<T> clazz, @Nullable T val);
 
         /**
          * Visit attribute. Attribute value is {@code boolean} primitive.
diff --git a/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CdcCommandLineStartup.java b/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CdcCommandLineStartup.java
index da5c84f..e7f82ba 100644
--- a/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CdcCommandLineStartup.java
+++ b/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CdcCommandLineStartup.java
@@ -17,15 +17,10 @@
 
 package org.apache.ignite.startup.cmdline;
 
-import java.net.URL;
-import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
-import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteSystemProperties;
-import org.apache.ignite.cdc.CdcConfiguration;
 import org.apache.ignite.cdc.CdcLoader;
 import org.apache.ignite.internal.cdc.CdcMain;
-import org.apache.ignite.internal.util.spring.IgniteSpringHelper;
 import org.apache.ignite.internal.util.typedef.X;
 import org.jetbrains.annotations.Nullable;
 
@@ -116,24 +111,6 @@
     }
 
     /**
-     * @param cfgUrl String configuration URL.
-     * @param spring Ignite spring helper.
-     * @return CDC consumer defined in spring configuration.
-     * @throws IgniteCheckedException in case of load error.
-     */
-    private static CdcConfiguration consumerConfig(
-        URL cfgUrl,
-        IgniteSpringHelper spring
-    ) throws IgniteCheckedException {
-        Map<Class<?>, Object> cdcCfgs = spring.loadBeans(cfgUrl, CdcConfiguration.class);
-
-        if (cdcCfgs == null || cdcCfgs.size() != 1)
-            exit("Exact 1 CaptureDataChangeConfiguration configuration should be defined", false, 1);
-
-        return (CdcConfiguration)cdcCfgs.values().iterator().next();
-    }
-
-    /**
      * Exists with optional error message, usage show and exit code.
      *
      * @param errMsg Optional error message.
diff --git a/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CommandLineStartup.java b/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CommandLineStartup.java
index cd67d36..300cc82 100644
--- a/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CommandLineStartup.java
+++ b/modules/core/src/main/java/org/apache/ignite/startup/cmdline/CommandLineStartup.java
@@ -39,6 +39,7 @@
 import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.IgnitionListener;
 import org.apache.ignite.SystemProperty;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.processors.cache.ExchangeContext;
 import org.apache.ignite.internal.processors.cache.GridCacheMapEntry;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.PartitionsEvictManager;
@@ -99,7 +100,8 @@
         CacheContinuousQueryEventBuffer.class,
         CacheContinuousQueryHandler.class,
         OffheapReadWriteLock.class,
-        TcpCommunicationConfiguration.class
+        TcpCommunicationConfiguration.class,
+        BinaryArray.class
     ));
 
     static {
diff --git a/modules/core/src/main/resources/META-INF/classnames.properties b/modules/core/src/main/resources/META-INF/classnames.properties
index 0b7f8b5..0406836 100644
--- a/modules/core/src/main/resources/META-INF/classnames.properties
+++ b/modules/core/src/main/resources/META-INF/classnames.properties
@@ -1675,10 +1675,6 @@
 org.apache.ignite.internal.processors.rest.protocols.tcp.redis.GridRedisMessage
 org.apache.ignite.internal.processors.rest.protocols.tcp.redis.GridRedisNioListener$1
 org.apache.ignite.internal.processors.rest.request.RestQueryRequest$QueryType
-org.apache.ignite.internal.processors.service.GridServiceAssignments
-org.apache.ignite.internal.processors.service.GridServiceAssignmentsKey
-org.apache.ignite.internal.processors.service.GridServiceDeployment
-org.apache.ignite.internal.processors.service.GridServiceDeploymentKey
 org.apache.ignite.internal.processors.service.GridServiceMethodNotFoundException
 org.apache.ignite.internal.processors.service.GridServiceNotFoundException
 org.apache.ignite.internal.processors.service.GridServiceProxy
@@ -1692,7 +1688,6 @@
 org.apache.ignite.internal.processors.service.ServiceContextImpl
 org.apache.ignite.internal.processors.service.ServiceDeploymentProcessId
 org.apache.ignite.internal.processors.service.ServiceDeploymentRequest
-org.apache.ignite.internal.processors.service.ServiceDescriptorImpl
 org.apache.ignite.internal.processors.service.ServiceInfo
 org.apache.ignite.internal.processors.service.ServiceProcessorCommonDiscoveryData
 org.apache.ignite.internal.processors.service.ServiceProcessorJoinNodeDiscoveryData
diff --git a/modules/core/src/test/java/org/apache/ignite/cdc/CdcCacheVersionTest.java b/modules/core/src/test/java/org/apache/ignite/cdc/CdcCacheVersionTest.java
index 050f4bc..efea107 100644
--- a/modules/core/src/test/java/org/apache/ignite/cdc/CdcCacheVersionTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/cdc/CdcCacheVersionTest.java
@@ -49,10 +49,16 @@
 import org.apache.ignite.plugin.AbstractTestPluginProvider;
 import org.apache.ignite.plugin.CachePluginContext;
 import org.apache.ignite.plugin.CachePluginProvider;
+import org.apache.ignite.spi.metric.IntMetric;
+import org.apache.ignite.spi.systemview.view.CacheView;
+import org.apache.ignite.spi.systemview.view.SystemView;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 
 import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+import static org.apache.ignite.internal.processors.cache.CacheMetricsImpl.CACHE_METRICS;
+import static org.apache.ignite.internal.processors.cache.ClusterCachesInfo.CACHES_VIEW;
+import static org.apache.ignite.internal.processors.cache.version.GridCacheVersionManager.DATA_VER_CLUSTER_ID;
 import static org.apache.ignite.testframework.GridTestUtils.runAsync;
 import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
 
@@ -122,6 +128,27 @@
         IgniteCache<Integer, User> cache = ign.getOrCreateCache(FOR_OTHER_CLUSTER_ID);
 
         addAndWaitForConsumption(cnsmr, cfg, cache, null, this::addConflictData, 0, KEYS_CNT, true);
+
+        assertEquals(
+            DFLT_CLUSTER_ID,
+            ign.context().metric().registry(CACHE_METRICS).<IntMetric>findMetric(DATA_VER_CLUSTER_ID).value()
+        );
+
+        boolean found = false;
+
+        SystemView<CacheView> caches = ign.context().systemView().view(CACHES_VIEW);
+
+        for (CacheView v : caches) {
+            if (v.cacheName().equals(FOR_OTHER_CLUSTER_ID)) {
+                assertEquals(v.conflictResolver(), "TestCacheConflictResolutionManager");
+
+                found = true;
+            }
+            else
+                assertNull(v.conflictResolver());
+        }
+
+        assertTrue(found);
     }
 
     /** */
@@ -223,6 +250,10 @@
 
                     return res;
                 }
+
+                @Override public String toString() {
+                    return "TestCacheConflictResolutionManager";
+                }
             };
         }
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/cdc/CdcSelfTest.java b/modules/core/src/test/java/org/apache/ignite/cdc/CdcSelfTest.java
index 11ab8f0..271e847 100644
--- a/modules/core/src/test/java/org/apache/ignite/cdc/CdcSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/cdc/CdcSelfTest.java
@@ -24,7 +24,6 @@
 import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
@@ -79,7 +78,11 @@
     public Supplier<MetricExporterSpi> metricExporter;
 
     /** */
-    @Parameterized.Parameters(name = "specificConsistentId={0}, walMode={1}, metricExporter={2}")
+    @Parameterized.Parameter(3)
+    public int pageSz;
+
+    /** */
+    @Parameterized.Parameters(name = "specificConsistentId={0},walMode={1},metricExporter={2},pageSz={3}")
     public static Collection<?> parameters() {
         List<Object[]> params = new ArrayList<>();
 
@@ -87,22 +90,19 @@
             for (boolean specificConsistentId : new boolean[] {false, true}) {
                 Supplier<MetricExporterSpi> jmx = JmxMetricExporterSpi::new;
 
-                params.add(new Object[] {specificConsistentId, mode, null});
-                params.add(new Object[] {specificConsistentId, mode, jmx});
+                params.add(new Object[] {specificConsistentId, mode, null, 0});
+                params.add(new Object[] {specificConsistentId, mode, jmx, 8192});
             }
 
         return params;
     }
 
-    /** Consistent id. */
-    private UUID consistentId = UUID.randomUUID();
-
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
 
         if (specificConsistentId)
-            cfg.setConsistentId(consistentId);
+            cfg.setConsistentId(igniteInstanceName);
 
         cfg.setDataStorageConfiguration(new DataStorageConfiguration()
             .setCdcEnabled(true)
@@ -110,6 +110,9 @@
             .setWalForceArchiveTimeout(WAL_ARCHIVE_TIMEOUT)
             .setDefaultDataRegionConfiguration(new DataRegionConfiguration().setPersistenceEnabled(true)));
 
+        if (pageSz != 0)
+            cfg.getDataStorageConfiguration().setPageSize(pageSz);
+
         cfg.setCacheConfiguration(
             new CacheConfiguration<>(TX_CACHE_NAME).setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)
         );
@@ -186,9 +189,7 @@
     /** */
     @Test
     public void testReadBeforeGracefulShutdown() throws Exception {
-        IgniteConfiguration cfg = getConfiguration("ignite-0");
-
-        Ignite ign = startGrid(cfg);
+        Ignite ign = startGrid(getConfiguration("ignite-0"));
 
         ign.cluster().state(ACTIVE);
 
@@ -210,7 +211,7 @@
             }
         };
 
-        CdcMain cdc = createCdc(cnsmr, cfg);
+        CdcMain cdc = createCdc(cnsmr, getConfiguration(ign.name()));
 
         runAsync(cdc);
 
@@ -246,9 +247,6 @@
     public void testMultiNodeConsumption() throws Exception {
         IgniteEx ign1 = startGrid(0);
 
-        if (specificConsistentId)
-            consistentId = UUID.randomUUID();
-
         IgniteEx ign2 = startGrid(1);
 
         ign1.cluster().state(ACTIVE);
@@ -272,8 +270,8 @@
         UserCdcConsumer cnsmr1 = new UserCdcConsumer();
         UserCdcConsumer cnsmr2 = new UserCdcConsumer();
 
-        IgniteConfiguration cfg1 = ign1.configuration();
-        IgniteConfiguration cfg2 = ign2.configuration();
+        IgniteConfiguration cfg1 = getConfiguration(ign1.name());
+        IgniteConfiguration cfg2 = getConfiguration(ign2.name());
 
         // Always run CDC with consistent id to ensure instance read data for specific node.
         if (!specificConsistentId) {
@@ -339,8 +337,8 @@
         UserCdcConsumer cnsmr1 = new UserCdcConsumer();
         UserCdcConsumer cnsmr2 = new UserCdcConsumer();
 
-        IgniteInternalFuture<?> fut1 = runAsync(createCdc(cnsmr1, ign.configuration()));
-        IgniteInternalFuture<?> fut2 = runAsync(createCdc(cnsmr2, ign.configuration()));
+        IgniteInternalFuture<?> fut1 = runAsync(createCdc(cnsmr1, getConfiguration(ign.name())));
+        IgniteInternalFuture<?> fut2 = runAsync(createCdc(cnsmr2, getConfiguration(ign.name())));
 
         assertTrue(waitForCondition(() -> fut1.isDone() || fut2.isDone(), getTestTimeout()));
 
@@ -361,9 +359,7 @@
     /** */
     @Test
     public void testReReadWhenStateWasNotStored() throws Exception {
-        IgniteConfiguration cfg = getConfiguration("ignite-0");
-
-        IgniteEx ign = startGrid(cfg);
+        IgniteEx ign = startGrid(getConfiguration("ignite-0"));
 
         ign.cluster().state(ACTIVE);
 
@@ -378,7 +374,7 @@
                 }
             };
 
-            CdcMain cdc = createCdc(cnsmr, cfg);
+            CdcMain cdc = createCdc(cnsmr, getConfiguration(ign.name()));
 
             IgniteInternalFuture<?> fut = runAsync(cdc);
 
@@ -424,7 +420,7 @@
             }
         };
 
-        CdcMain cdc = createCdc(cnsmr, cfg);
+        CdcMain cdc = createCdc(cnsmr, getConfiguration(ign.name()));
 
         IgniteInternalFuture<?> fut = runAsync(cdc);
 
@@ -442,7 +438,7 @@
 
         consumeHalf.set(false);
 
-        cdc = createCdc(cnsmr, cfg);
+        cdc = createCdc(cnsmr, getConfiguration(ign.name()));
 
         fut = runAsync(cdc);
 
diff --git a/modules/core/src/test/java/org/apache/ignite/client/FunctionalTest.java b/modules/core/src/test/java/org/apache/ignite/client/FunctionalTest.java
index 59a4120..19505e7 100644
--- a/modules/core/src/test/java/org/apache/ignite/client/FunctionalTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/client/FunctionalTest.java
@@ -65,7 +65,9 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.binary.AbstractBinaryArraysTest;
 import org.apache.ignite.internal.client.thin.ClientServerError;
+import org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum;
 import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor;
 import org.apache.ignite.internal.processors.platform.cache.expiry.PlatformExpiryPolicy;
 import org.apache.ignite.internal.processors.platform.client.ClientStatus;
@@ -82,27 +84,23 @@
 import org.junit.Test;
 import org.junit.rules.Timeout;
 
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL1;
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL2;
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL3;
 import static org.apache.ignite.internal.processors.cache.transactions.IgniteTxManager.TXS_MON_LIST;
 import static org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
 import static org.apache.ignite.testframework.GridTestUtils.runAsync;
-import static org.apache.ignite.testframework.junits.GridAbstractTest.getMxBean;
 import static org.apache.ignite.transactions.TransactionConcurrency.OPTIMISTIC;
 import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC;
 import static org.apache.ignite.transactions.TransactionIsolation.READ_COMMITTED;
 import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ;
 import static org.apache.ignite.transactions.TransactionIsolation.SERIALIZABLE;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 /**
  * Thin client functional tests.
  */
-public class FunctionalTest {
+public class FunctionalTest extends AbstractBinaryArraysTest {
     /** Per test timeout */
     @SuppressWarnings("deprecation")
     @Rule
@@ -365,7 +363,7 @@
             checkDataType(client, ignite, new Date());
 
             // Enum.
-            checkDataType(client, ignite, CacheAtomicityMode.ATOMIC);
+            checkDataType(client, ignite, VAL1);
 
             // Binary object.
             checkDataType(client, ignite, person);
@@ -384,7 +382,7 @@
             checkDataType(client, ignite, new Date[] {new Date()});
             checkDataType(client, ignite, new int[][] {new int[] {1}});
 
-            checkDataType(client, ignite, new CacheAtomicityMode[] {CacheAtomicityMode.ATOMIC});
+            checkDataType(client, ignite, new TestEnum[] {VAL1, VAL2, VAL3});
 
             checkDataType(client, ignite, new Person[] {person});
             checkDataType(client, ignite, new Person[][] {new Person[] {person}});
@@ -441,13 +439,11 @@
 
         assertEquals(client.binary().typeId(obj.getClass().getName()), ignite.binary().typeId(obj.getClass().getName()));
 
-        if (!obj.getClass().isArray()) { // TODO IGNITE-12578
-            // Server-side comparison with the original object.
-            assertTrue(thinCache.replace(key, obj, obj));
+        // Server-side comparison with the original object.
+        assertTrue(thinCache.replace(key, obj, obj));
 
-            // Server-side comparison with the restored object.
-            assertTrue(thinCache.remove(key, cachedObj));
-        }
+        // Server-side comparison with the restored object.
+        assertTrue(thinCache.remove(key, cachedObj));
     }
 
     /**
@@ -456,7 +452,7 @@
      * @param exp Expected value.
      * @param actual Actual value.
      */
-    private void assertEqualsArraysAware(Object exp, Object actual) {
+    public static void assertEqualsArraysAware(Object exp, Object actual) {
         if (exp instanceof Object[])
             assertArrayEquals((Object[])exp, (Object[])actual);
         else if (U.isPrimitiveArray(exp))
diff --git a/modules/core/src/test/java/org/apache/ignite/client/ReliabilityTest.java b/modules/core/src/test/java/org/apache/ignite/client/ReliabilityTest.java
index 6a8516a..9d00c85 100644
--- a/modules/core/src/test/java/org/apache/ignite/client/ReliabilityTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/client/ReliabilityTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.client;
 
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -31,6 +32,7 @@
 import java.util.stream.Stream;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
 import org.apache.ignite.Ignition;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheMode;
@@ -40,6 +42,7 @@
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.failure.FailureHandler;
 import org.apache.ignite.internal.client.thin.AbstractThinClientTest;
+import org.apache.ignite.internal.client.thin.ClientOperation;
 import org.apache.ignite.internal.client.thin.ClientServerError;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.services.Service;
@@ -193,6 +196,132 @@
     }
 
     /**
+     * Test single server can be used multiple times in configuration.
+     */
+    @Test
+    public void testRetryReadPolicyRetriesCacheGet() {
+        try (LocalIgniteCluster cluster = LocalIgniteCluster.start(1);
+             IgniteClient client = Ignition.startClient(getClientConfiguration()
+                 .setRetryPolicy(new ClientRetryReadPolicy())
+                 .setAddresses(
+                     cluster.clientAddresses().iterator().next(),
+                     cluster.clientAddresses().iterator().next()))
+        ) {
+            ClientCache<Integer, Integer> cache = client.createCache("cache");
+
+            // Before fail.
+            cachePut(cache, 0, 0);
+
+            // Fail.
+            dropAllThinClientConnections(Ignition.allGrids().get(0));
+
+            // Reuse second address without fail.
+            cache.get(0);
+        }
+    }
+
+    /**
+     * Tests that retry limit of 1 effectively disables retry/failover.
+     */
+    @SuppressWarnings("ThrowableNotThrown")
+    @Test
+    public void testRetryLimitDisablesFailover() {
+        try (LocalIgniteCluster cluster = LocalIgniteCluster.start(1);
+             IgniteClient client = Ignition.startClient(getClientConfiguration()
+                 .setRetryLimit(1)
+                 .setAddresses(
+                     cluster.clientAddresses().iterator().next(),
+                     cluster.clientAddresses().iterator().next()))
+        ) {
+            ClientCache<Integer, Integer> cache = client.createCache("cache");
+
+            // Before fail.
+            cachePut(cache, 0, 0);
+
+            // Fail.
+            dropAllThinClientConnections(Ignition.allGrids().get(0));
+
+            // Reuse second address without fail.
+            GridTestUtils.assertThrows(null, () -> cachePut(cache, 0, 0), IgniteException.class,
+                    "Channel is closed");
+        }
+    }
+
+    /**
+     * Tests that setting retry policy to null effectively disables retry/failover.
+     */
+    @SuppressWarnings("ThrowableNotThrown")
+    @Test
+    public void testNullRetryPolicyDisablesFailover() {
+        try (LocalIgniteCluster cluster = LocalIgniteCluster.start(1);
+             IgniteClient client = Ignition.startClient(getClientConfiguration()
+                 .setRetryPolicy(null)
+                 .setAddresses(
+                     cluster.clientAddresses().iterator().next(),
+                     cluster.clientAddresses().iterator().next()))
+        ) {
+            ClientCache<Integer, Integer> cache = client.createCache("cache");
+
+            // Before fail.
+            cachePut(cache, 0, 0);
+
+            // Fail.
+            dropAllThinClientConnections(Ignition.allGrids().get(0));
+
+            // Reuse second address without fail.
+            GridTestUtils.assertThrows(null, () -> cachePut(cache, 0, 0), IgniteException.class,
+                    "Channel is closed");
+        }
+    }
+
+    /**
+     * Tests that retry limit of 1 effectively disables retry/failover.
+     */
+    @SuppressWarnings("ThrowableNotThrown")
+    @Test
+    public void testRetryNonePolicyDisablesFailover() {
+        try (LocalIgniteCluster cluster = LocalIgniteCluster.start(1);
+             IgniteClient client = Ignition.startClient(getClientConfiguration()
+                 .setRetryPolicy(new ClientRetryNonePolicy())
+                 .setAddresses(
+                     cluster.clientAddresses().iterator().next(),
+                     cluster.clientAddresses().iterator().next()))
+        ) {
+            ClientCache<Integer, Integer> cache = client.createCache("cache");
+
+            // Before fail.
+            cachePut(cache, 0, 0);
+
+            // Fail.
+            dropAllThinClientConnections(Ignition.allGrids().get(0));
+
+            // Reuse second address without fail.
+            GridTestUtils.assertThrows(null, () -> cachePut(cache, 0, 0), IgniteException.class,
+                    "Channel is closed");
+        }
+    }
+
+    /**
+     * Tests that {@link ClientOperationType} is updated accordingly when {@link ClientOperation} is added.
+     */
+    @Test
+    public void testRetryPolicyConvertOpAllOperationsSupported() {
+        List<ClientOperation> nullOps = Arrays.stream(ClientOperation.values())
+                .filter(o -> o.toPublicOperationType() == null)
+                .collect(Collectors.toList());
+
+        String nullOpsNames = nullOps.stream().map(Enum::name).collect(Collectors.joining(", "));
+
+        long expectedNullCount = 12;
+
+        String msg = expectedNullCount
+                + " operation codes do not have public equivalent. When adding new codes, update ClientOperationType too. Missing ops: "
+                + nullOpsNames;
+
+        assertEquals(msg, expectedNullCount, nullOps.size());
+    }
+
+    /**
      * Test that failover doesn't lead to silent query inconsistency.
      */
     @Test
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/GridMultithreadedJobStealingSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/GridMultithreadedJobStealingSelfTest.java
index fcee6df..be45162 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/GridMultithreadedJobStealingSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/GridMultithreadedJobStealingSelfTest.java
@@ -68,14 +68,41 @@
 
     /** {@inheritDoc} */
     @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        stopAllGrids();
+
         ignite = startGridsMultiThreaded(2);
     }
 
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
-        ignite = null;
+        super.afterTest();
 
         stopAllGrids();
+
+        ignite = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        JobStealingCollisionSpi colSpi = new JobStealingCollisionSpi();
+
+        // One job at a time.
+        colSpi.setActiveJobsThreshold(1);
+        colSpi.setWaitJobsThreshold(0);
+
+        JobStealingFailoverSpi failSpi = new JobStealingFailoverSpi();
+
+        // Verify defaults.
+        assert failSpi.getMaximumFailoverAttempts() == JobStealingFailoverSpi.DFLT_MAX_FAILOVER_ATTEMPTS;
+
+        cfg.setCollisionSpi(colSpi);
+        cfg.setFailoverSpi(failSpi);
+
+        return cfg;
     }
 
     /**
@@ -94,7 +121,9 @@
         int threadsNum = 10;
 
         GridTestUtils.runMultiThreaded(new Runnable() {
-            /** */
+            /**
+             *
+             */
             @Override public void run() {
                 try {
                     JobStealingResult res = ignite.compute().execute(new JobStealingTask(2), null);
@@ -122,7 +151,7 @@
         // Total jobs number is threadsNum * 2
         assertEquals("Incorrect processed jobs number", threadsNum * 2, stolen.get() + noneStolen.get());
 
-        assertFalse( "No jobs were stolen.", stolen.get() == 0);
+        assertFalse("No jobs were stolen.", stolen.get() == 0);
 
         for (Ignite g : G.allGrids())
             assertTrue("Node get no jobs.", nodes.contains(g.name()));
@@ -130,7 +159,7 @@
         // Under these circumstances we should not have  more than 2 jobs
         // difference.
         //(but muted to 4 due to very rare fails and low priority of fix)
-        assertTrue( "Stats [stolen=" + stolen + ", noneStolen=" + noneStolen + ']',
+        assertTrue("Stats [stolen=" + stolen + ", noneStolen=" + noneStolen + ']',
             Math.abs(stolen.get() - noneStolen.get()) <= 4);
     }
 
@@ -154,7 +183,9 @@
         jobExecutedLatch = new CountDownLatch(threadsNum);
 
         final IgniteInternalFuture<Long> future = GridTestUtils.runMultiThreadedAsync(new Runnable() {
-            /** */
+            /**
+             *
+             */
             @Override public void run() {
                 try {
                     final IgniteCompute compute = ignite.compute().withAsync();
@@ -190,38 +221,16 @@
 
         assertNull("Test failed with exception: ", fail.get());
 
-        // Total jobs number is threadsNum * 3
+        // Total jobs number is threadsNum * 4
         assertEquals("Incorrect processed jobs number", threadsNum * jobsPerTask, stolen.get() + noneStolen.get());
 
-        assertFalse( "No jobs were stolen.", stolen.get() == 0);
+        assertFalse("No jobs were stolen.", stolen.get() == 0);
 
         for (Ignite g : G.allGrids())
             assertTrue("Node get no jobs.", nodes.contains(g.name()));
 
-        assertTrue( "Stats [stolen=" + stolen + ", noneStolen=" + noneStolen + ']',
-            Math.abs(stolen.get() - 2 * noneStolen.get()) <= 6);
-    }
-
-
-    /** {@inheritDoc} */
-    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
-        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
-
-        JobStealingCollisionSpi colSpi = new JobStealingCollisionSpi();
-
-        // One job at a time.
-        colSpi.setActiveJobsThreshold(1);
-        colSpi.setWaitJobsThreshold(0);
-
-        JobStealingFailoverSpi failSpi = new JobStealingFailoverSpi();
-
-        // Verify defaults.
-        assert failSpi.getMaximumFailoverAttempts() == JobStealingFailoverSpi.DFLT_MAX_FAILOVER_ATTEMPTS;
-
-        cfg.setCollisionSpi(colSpi);
-        cfg.setFailoverSpi(failSpi);
-
-        return cfg;
+        assertTrue("Stats [stolen=" + stolen + ", noneStolen=" + noneStolen + ']',
+            Math.abs(stolen.get() - 2 * noneStolen.get()) <= 8);
     }
 
     /**
@@ -300,7 +309,10 @@
         /** {@inheritDoc} */
         @Override public Serializable execute() {
             try {
-                jobExecutedLatch.countDown();
+                CountDownLatch latch = jobExecutedLatch;
+
+                if (latch != null)
+                    latch.countDown();
 
                 Long sleep = argument(0);
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/AbstractBinaryArraysTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/AbstractBinaryArraysTest.java
new file mode 100644
index 0000000..b5f3173
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/AbstractBinaryArraysTest.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.internal.binary;
+
+import java.util.Arrays;
+import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+
+/** Base test to check both mode for {@link IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS}. */
+@RunWith(Parameterized.class)
+public abstract class AbstractBinaryArraysTest extends GridCommonAbstractTest {
+    /** Generates values for the {@link #useBinaryArrays} parameter. */
+    @Parameterized.Parameters(name = "useBinaryArrays = {0}")
+    public static Iterable<Object[]> useBinaryArrays() {
+        return Arrays.asList(new Object[][] {{true}, {false}});
+    }
+
+    /** @see IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS */
+    @Parameterized.Parameter
+    public boolean useBinaryArrays;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(useBinaryArrays));
+        BinaryArray.initUseBinaryArrays();
+
+        super.beforeTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        System.clearProperty(IGNITE_USE_BINARY_ARRAYS);
+        BinaryArray.initUseBinaryArrays();
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryArraySelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryArraySelfTest.java
new file mode 100644
index 0000000..e487ba4
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryArraySelfTest.java
@@ -0,0 +1,650 @@
+/*
+ * 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.binary;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.client.ClientCache;
+import org.apache.ignite.client.Config;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.internal.binary.BinaryMarshallerSelfTest.TestClass1;
+import org.apache.ignite.internal.binary.mutabletest.GridBinaryTestClasses.TestObjectContainer;
+import org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum;
+import org.apache.ignite.internal.processors.platform.utils.PlatformUtils;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.junit.Test;
+
+import static org.apache.ignite.client.FunctionalTest.assertEqualsArraysAware;
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL1;
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL2;
+import static org.apache.ignite.internal.processors.cache.CacheEnumOperationsAbstractTest.TestEnum.VAL3;
+import static org.junit.Assert.assertArrayEquals;
+
+/** */
+public class BinaryArraySelfTest extends AbstractBinaryArraysTest {
+    /** */
+    private static Ignite server;
+
+    /** */
+    private static Ignite client;
+
+    /** */
+    private static CacheAdapter<Object, Object> srvCache;
+
+    /** */
+    private static CacheAdapter<Object, Object> cliCache;
+
+    /** */
+    private static final Function<Object, Object> TO_TEST_CLS = arr -> arr instanceof TestClass1[][]
+        ? new TestClass2(null, (TestClass1[][])arr)
+        : new TestClass2((TestClass1[])arr, null);
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        server = startGrid(0);
+        client = startClientGrid(1);
+
+        srvCache = new IgniteCacheAdapter<>(server.createCache(DEFAULT_CACHE_NAME));
+        cliCache = new IgniteCacheAdapter<>(client.getOrCreateCache(DEFAULT_CACHE_NAME));
+    }
+
+    /** */
+    @Test
+    public void testArrayKey() {
+        doTestKeys(srvCache, arr -> arr);
+        doTestKeys(cliCache, arr -> arr);
+        try (IgniteClient thinClient = thinClient()) {
+            // Using other cache because
+            // 1. Removed entry store in `GridCacheAdapter#map`
+            // 2. Bytes representation of multidimensional BinaryArray from thin client and Ignite node differ.
+            // 2a. Thin client reads array from byte stream and preserve pointer equality (look at #dataToTest() -> arr6).
+            // 2b. Client node invoke `CacheObjectBinaryProcessorImpl#marshallToBinary` before storing
+            // which breaks link equality of array
+            // 3. During invocation of `put` node inserts key representation from previous invocation of methods from client node.
+            // 4. This lead to `ClientCache#containsKey` can't find key because
+            // it invokes search based equality on `byte[]` key representaion.
+            //
+            // This doesn't happen in `useBinaryArrays=false` becuase Ignite node obtain `Object[]`
+            // from byte stream and invoke marshallToBinary for it which also breaks pointer equality
+            // therefore during serialization handle will NOT be used.
+            // In `useBinaryArrays=true` node read `BinaryArray` from stream which mean no need to marshall to binary
+            // therefore link equality preserved which mean during serialization handle will be used.
+            doTestKeys(
+                new ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME + (useBinaryArrays ? "2" : ""))),
+                arr -> arr
+            );
+        }
+    }
+
+    /** */
+    @Test
+    public void testArrayFieldInKey() {
+        doTestKeys(srvCache, TO_TEST_CLS);
+        doTestKeys(cliCache, TO_TEST_CLS);
+        try (IgniteClient thinClient = thinClient()) {
+            doTestKeys(new ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME)), TO_TEST_CLS);
+        }
+    }
+
+    /** */
+    @Test
+    public void testArrayValue() {
+        doTestValue(srvCache, arr -> arr, false, false);
+        doTestValue(cliCache, arr -> arr, false, false);
+        doTestValue(srvCache, arr -> arr, true, false);
+        doTestValue(cliCache, arr -> arr, true, false);
+
+        try (IgniteClient thinClient = thinClient()) {
+            ClientCacheAdapter<Object, Object> c =
+                new ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME));
+
+            doTestValue(c, arr -> arr, false, false);
+            doTestValue(c, arr -> arr, true, false);
+        }
+    }
+
+    /** */
+    @Test
+    public void testArrayFieldInValue() {
+        doTestValue(srvCache, TO_TEST_CLS, false, true);
+        doTestValue(cliCache, TO_TEST_CLS, false, true);
+        doTestValue(srvCache, TO_TEST_CLS, true, true);
+        doTestValue(cliCache, TO_TEST_CLS, true, true);
+        try (IgniteClient thinClient = thinClient()) {
+            ClientCacheAdapter<Object, Object> c =
+                new ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME));
+
+            doTestValue(c, TO_TEST_CLS, false, true);
+            doTestValue(c, TO_TEST_CLS, true, true);
+        }
+    }
+
+    /** */
+    @Test
+    public void testArraySerDe() {
+        checkArraySerDe(arr -> {
+            Object obj = server.binary().toBinary(arr);
+
+            return useBinaryArrays
+                ? ((BinaryObject)obj).deserialize()
+                : PlatformUtils.unwrapBinariesInArray((Object[])obj);
+        }, false);
+    }
+
+    /** */
+    @Test
+    public void testArrayFieldSerDe() {
+        checkArraySerDe(arr -> {
+            BinaryObject bObj = server.binary().toBinary(new TestObjectContainer(arr));
+
+            return ((TestObjectContainer)bObj.deserialize()).foo;
+        }, true);
+    }
+
+    /** */
+    @Test
+    public void testBinaryModeArray() {
+        putInBinaryGetRegular(srvCache);
+        putInBinaryGetRegular(cliCache);
+        try (IgniteClient thinClient = thinClient()) {
+            putInBinaryGetRegular(new ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME)));
+        }
+    }
+
+    /** */
+    @Test
+    public void testPrimitivesArrays() {
+        doTestBoxedPrimitivesArrays(srvCache);
+        doTestBoxedPrimitivesArrays(cliCache);
+
+        doTestPrimitivesArrays(srvCache);
+        doTestPrimitivesArrays(cliCache);
+
+        try (IgniteClient thinClient = thinClient()) {
+            ClientCacheAdapter<Object, Object> c = new
+                ClientCacheAdapter<>(thinClient.getOrCreateCache(DEFAULT_CACHE_NAME));
+
+            doTestBoxedPrimitivesArrays(c);
+            doTestPrimitivesArrays(c);
+        }
+    }
+
+    /** */
+    @Test
+    public void testSimpleBinaryArrayFieldSerDe() {
+        TestClass1 src = new TestClass1();
+
+        BinaryMarshallerSelfTest.SimpleObject sobj = GridTestUtils.getFieldValue(src, "obj");
+
+        GridTestUtils.setFieldValue(sobj, "objArr", new Object[] {"string", 1L, null});
+
+        BinaryObject obj = server.binary().toBinary(src);
+
+        BinaryObject simpleObj = obj.field("obj");
+        Object objArr = simpleObj.field("objArr");
+
+        assertEquals(useBinaryArrays ? BinaryArray.class : Object[].class, objArr.getClass());
+
+        Object deser = obj.deserialize();
+
+        assertEquals(TestClass1.class, deser.getClass());
+
+        sobj = GridTestUtils.getFieldValue(deser, "obj");
+
+        Object[] arr = GridTestUtils.getFieldValue(sobj, "objArr");
+
+        assertNotNull(arr);
+        assertEquals(3, arr.length);
+        assertEquals("string", arr[0]);
+        assertEquals(1L, arr[1]);
+        assertNull(arr[2]);
+    }
+
+    /** */
+    @Test
+    public void testArrayOfCollectionSerDe() {
+        List<TestClass1> l1 = new ArrayList<>(F.asList(new TestClass1(), new TestClass1()));
+        List<TestClass1> l2 = new ArrayList<>(F.asList(new TestClass1(), new TestClass1()));
+        List<TestClass1> l3 = new ArrayList<>(F.asList(new TestClass1(), new TestClass1()));
+
+        List[] arr = new List[] { l1, l2, l3 };
+
+        Object res = server.binary().toBinary(arr);
+        Object[] res0;
+
+        if (useBinaryArrays) {
+            assertTrue(res instanceof BinaryArray);
+
+            res0 = ((BinaryArray)res).deserialize();
+        }
+        else {
+            assertTrue(res instanceof Object[]);
+
+            res0 = PlatformUtils.unwrapBinariesInArray((Object[])res);
+        }
+
+        assertEquals(arr.length, res0.length);
+
+        for (int i = 0; i < arr.length; i++)
+            assertEqualsCollections(arr[i], (Collection<?>)res0[i]);
+    }
+
+    /** */
+    @Test
+    public void testArrayOfBinariesSerDe() {
+        BinaryObject[] arr = new BinaryObject[] {
+            server.binary().toBinary(new TestClass1()),
+            server.binary().toBinary(new TestClass1())
+        };
+
+        Object obj = server.binary().toBinary(arr);
+
+        Object deser;
+
+        if (useBinaryArrays) {
+            assertTrue(obj instanceof BinaryArray);
+
+            deser = ((BinaryArray)obj).deserialize();
+        }
+        else {
+            assertTrue(obj instanceof Object[]);
+
+            deser = PlatformUtils.unwrapBinariesInArray((Object[])obj);
+        }
+
+        assertEquals(Object[].class, deser.getClass());
+
+        Object[] res = ((Object[])deser);
+
+        assertEquals(2, res.length);
+        assertTrue(res[0] instanceof TestClass1);
+        assertTrue(res[1] instanceof TestClass1);
+    }
+
+    /** */
+    private void doTestKeys(CacheAdapter<Object, Object> c, Function<Object, Object> wrap) {
+        List<?> keys = dataToTest();
+
+        for (int i = 0; i < keys.size(); i++) {
+            Object key = wrap.apply(keys.get(i));
+
+            assertFalse(c.containsKey(key));
+
+            c.put(key, i);
+
+            assertTrue(c.containsKey(key));
+            assertEquals(i, c.get(key));
+            assertTrue(c.replace(key, i, i + 1));
+            assertEquals(i + 1, c.get(key));
+            assertTrue(c.remove(key));
+        }
+    }
+
+    /** */
+    private void doTestValue(
+        CacheAdapter<Object, Object> c,
+        Function<Object, Object> wrap,
+        boolean keepBinary,
+        boolean alwaysSameType
+    ) {
+        AtomicInteger cntr = new AtomicInteger();
+
+        checkArraySerDe(arr -> {
+            c.put(cntr.getAndIncrement(), wrap.apply(arr));
+
+            Object obj;
+
+            if (keepBinary) {
+                obj = c.withKeepBinary().get(cntr.get() - 1);
+
+                if (obj instanceof BinaryObject)
+                    obj = ((BinaryObject)obj).deserialize();
+                else
+                    obj = PlatformUtils.unwrapBinariesInArray((Object[])obj);
+            }
+            else
+                obj = c.get(cntr.get() - 1);
+
+            if (obj instanceof Object[])
+                return obj;
+
+            TestClass2 cached = (TestClass2)obj;
+
+            return cached.arr != null ? cached.arr : cached.arr2;
+        }, alwaysSameType);
+
+        List<?> vals = dataToTest();
+
+        for (int i = 0; i < vals.size(); i++)
+            assertTrue(c.replace(i, wrap.apply(vals.get(i)), wrap.apply(vals.get((i + 1) % vals.size()))));
+
+        for (int i = 0; i < cntr.get(); i++)
+            assertTrue(c.remove(i));
+    }
+
+    /** */
+    private List<?> dataToTest() {
+        TestClass1[][] arr5 = new TestClass1[3][2];
+        TestClass1[][] arr6 = new TestClass1[2][2];
+
+        arr5[0] = new TestClass1[] {new TestClass1()};
+        arr5[1] = new TestClass1[] {new TestClass1()};
+
+        // arr6[0] == arr6[1]
+        arr6[0] = new TestClass1[] {new TestClass1()};
+        arr6[1] = arr6[0];
+
+        return F.asList(
+            new TestClass1[0],
+            new TestClass1[1][2],
+            new TestClass1[] {new TestClass1(), new TestClass1()},
+            arr5,
+            arr6
+        );
+    }
+
+    /** */
+    private void putRegularGetInBinary(IgniteCache<Object, Object> c) {
+        List<?> vals = dataToTest();
+
+        for (Object val : vals) {
+            c.put(1, val);
+
+            Object obj = c.withKeepBinary().get(1);
+
+            assertEquals(useBinaryArrays ? BinaryArray.class : Object[].class, obj.getClass());
+
+            if (useBinaryArrays)
+                assertEquals(val.getClass(), ((BinaryObject)obj).deserialize().getClass());
+
+            assertTrue(c.remove(1));
+        }
+    }
+
+    /** */
+    private void putInBinaryGetRegular(CacheAdapter<Object, Object> c) {
+        Runnable checker = () -> {
+            Object[] arr = (Object[])c.get(1);
+
+            assertTrue(arr[0] instanceof TestClass1);
+            assertTrue(arr[1] instanceof TestClass1);
+
+            assertTrue(c.withKeepBinary().remove(1));
+
+            assertNull(c.withKeepBinary().get(1));
+            assertNull(c.get(1));
+        };
+
+        {
+            BinaryObject[] src = new BinaryObject[] {
+                server.binary().toBinary(new TestClass1()),
+                server.binary().toBinary(new TestClass1())
+            };
+
+            c.withKeepBinary().put(1, src);
+
+            checker.run();
+        }
+
+        {
+            Object src = server.binary().toBinary(
+                new Object[] {
+                    server.binary().toBinary(new TestClass1()),
+                    server.binary().toBinary(new TestClass1())
+                }
+            );
+
+            assertEquals(useBinaryArrays ? BinaryArray.class : Object[].class, src.getClass());
+
+            c.withKeepBinary().put(1, src);
+
+            checker.run();
+        }
+    }
+
+    /** */
+    private void doTestPrimitivesArrays(CacheAdapter<Object, Object> c) {
+        Object[] data = new Object[] {
+            new byte[] {1, 2, 3},
+            new short[] {1, 2, 3},
+            new int[] {1, 2, 3},
+            new long[] {1L, 2L, 3L},
+            new float[] {1f, 2f, 3f},
+            new double[] {1d, 2d, 3d},
+            new char[] {'a', 'b', 'c'},
+            new boolean[] {true, false}
+        };
+
+        for (Object item : data) {
+            c.put(1, item);
+
+            Object item0 = c.get(1);
+
+            assertTrue(c.replace(1, item, item));
+            assertTrue(c.remove(1));
+            assertEquals(item.getClass(), item0.getClass());
+            assertEqualsArraysAware(item, item0);
+
+            c.put(item, 1);
+
+            assertTrue(c.containsKey(item));
+            assertEquals(1, c.get(item));
+            assertTrue(c.replace(item, 1, 2));
+            assertTrue(c.remove(item));
+        }
+    }
+
+    /** */
+    private void doTestBoxedPrimitivesArrays(CacheAdapter<Object, Object> c) {
+        Object[] data = new Object[] {
+            new Byte[] {1, 2, 3},
+            new Short[] {1, 2, 3},
+            new Integer[] {1, 2, 3},
+            new Long[] {1L, 2L, 3L},
+            new Float[] {1f, 2f, 3f},
+            new Double[] {1d, 2d, 3d},
+            new Character[] {'a', 'b', 'c'},
+            new Boolean[] {true, false},
+            new TestEnum[] {VAL1, VAL2, VAL3}
+        };
+
+        for (Object item : data) {
+            c.put(1, item);
+
+            Object item0 = c.get(1);
+
+            assertTrue(c.replace(1, item, item));
+            assertTrue(c.remove(1));
+
+            if (useBinaryArrays)
+                assertEquals(item.getClass(), item0.getClass());
+
+            assertTrue(Arrays.equals((Object[])item, (Object[])item0));
+
+            c.put(item, 1);
+
+            assertTrue(c.containsKey(item));
+            assertEquals(1, c.get(item));
+            assertTrue(c.replace(item, 1, 2));
+            assertTrue(c.remove(item));
+
+        }
+    }
+
+    /** */
+    public void checkArraySerDe(Function<Object[], Object> serde, boolean sameArr) {
+        for (Object[] arr : (List<Object[]>)dataToTest()) {
+            Object deser = serde.apply(arr);
+
+            assertEquals((useBinaryArrays || sameArr) ? arr.getClass() : Object[].class, deser.getClass());
+
+            assertEquals(arr.length, ((Object[])deser).length);
+            assertArrayEquals(arr, ((Object[])deser));
+
+            if (arr instanceof TestClass1[][] && arr.length == 2 && sameArr) {
+                Object[] val = (Object[])deser;
+
+                // See dataToTest -> dataToTest -> arr6[0] == arr6[1]
+                // Check the sanity of test data.
+                assertSame(((TestClass1[][])arr)[0], ((TestClass1[][])arr)[1]);
+                assertSame(val[0], val[1]);
+            }
+        }
+    }
+
+    /** */
+    private IgniteClient thinClient() {
+        return Ignition.startClient(new ClientConfiguration()
+            .setAddresses(Config.SERVER)
+            .setBinaryConfiguration(new BinaryConfiguration().setCompactFooter(true))
+        );
+    }
+
+    /** */
+    public static class TestClass2 {
+        /** */
+        final TestClass1[] arr;
+
+        /** */
+        final TestClass1[][] arr2;
+
+        /** */
+        public TestClass2(TestClass1[] arr, TestClass1[][] arr2) {
+            this.arr = arr;
+            this.arr2 = arr2;
+        }
+    }
+
+    /** */
+    public interface CacheAdapter<K, V> {
+        /** */
+        public V get(K key);
+
+        /** */
+        public void put(K key, V val);
+
+        /** */
+        public boolean replace(K key, V oldVal, V newVal);
+
+        /** */
+        public boolean remove(K key);
+
+        /** */
+        public boolean containsKey(K key);
+
+        /** */
+        public <K1, V1> CacheAdapter<K1, V1> withKeepBinary();
+    }
+
+    /** */
+    private static class IgniteCacheAdapter<K, V> implements CacheAdapter<K, V> {
+        /** */
+        private final IgniteCache<K, V> c;
+
+        /** */
+        public IgniteCacheAdapter(IgniteCache<K, V> c) {
+            this.c = c;
+        }
+
+        /** {@inheritDoc} */
+        @Override public V get(K key) {
+            return c.get(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void put(K key, V val) {
+            c.put(key, val);
+
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean replace(K key, V oldVal, V newVal) {
+            return c.replace(key, oldVal, newVal);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean remove(K key) {
+            return c.remove(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean containsKey(K key) {
+            return c.containsKey(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public <K1, V1> CacheAdapter<K1, V1> withKeepBinary() {
+            return new IgniteCacheAdapter<>(c.withKeepBinary());
+        }
+    }
+
+    /** */
+    private static class ClientCacheAdapter<K, V> implements CacheAdapter<K, V> {
+        /** */
+        private final ClientCache<K, V> c;
+
+        /** */
+        public ClientCacheAdapter(ClientCache<K, V> c) {
+            this.c = c;
+        }
+
+        /** {@inheritDoc} */
+        @Override public V get(K key) {
+            return c.get(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void put(K key, V val) {
+            c.put(key, val);
+
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean replace(K key, V oldVal, V newVal) {
+            return c.replace(key, oldVal, newVal);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean remove(K key) {
+            return c.remove(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean containsKey(K key) {
+            return c.containsKey(key);
+        }
+
+        /** {@inheritDoc} */
+        @Override public <K1, V1> CacheAdapter<K1, V1> withKeepBinary() {
+            return new ClientCacheAdapter<>(c.withKeepBinary());
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryEnumsSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryEnumsSelfTest.java
index 0577770..0ded707 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryEnumsSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryEnumsSelfTest.java
@@ -37,14 +37,13 @@
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 /**
  * Contains tests for binary enums.
  */
 @SuppressWarnings("unchecked")
-public class BinaryEnumsSelfTest extends GridCommonAbstractTest {
+public class BinaryEnumsSelfTest extends AbstractBinaryArraysTest {
     /** Cache name. */
     private static String CACHE_NAME = "cache";
 
@@ -74,11 +73,15 @@
 
     /** {@inheritDoc} */
     @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
         register = false;
     }
 
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
+        super.beforeTest();
+
         stopAllGrids();
     }
 
@@ -617,8 +620,15 @@
             assertEquals(EnumType.TWO, arr2[1]);
         }
 
-        Object[] arrBinary1 = (Object[])cacheBinary1.get(1);
-        Object[] arrBinary2 = (Object[])cacheBinary2.get(1);
+        Object arr1 = cacheBinary1.get(1);
+        Object arr2 = cacheBinary2.get(1);
+
+        Object[] arrBinary1 = useBinaryArrays
+            ? ((BinaryArray)arr1).array()
+            : (Object[])arr1;
+        Object[] arrBinary2 = useBinaryArrays
+            ? ((BinaryArray)arr2).array()
+            : (Object[])arr2;
 
         assertEquals(2, arrBinary1.length);
         assertEquals(2, arrBinary2.length);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldExtractionSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldExtractionSelfTest.java
index 55ebd7c..94f3c79 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldExtractionSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldExtractionSelfTest.java
@@ -25,7 +25,6 @@
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.configuration.BinaryConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -58,7 +57,7 @@
 
         marsh.setContext(new MarshallerContextTestImpl(null));
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
 
         return marsh;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldsAbstractSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldsAbstractSelfTest.java
index d6abe5f..a9d5657 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldsAbstractSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFieldsAbstractSelfTest.java
@@ -27,7 +27,7 @@
 import org.apache.ignite.binary.BinaryTypeConfiguration;
 import org.apache.ignite.configuration.BinaryConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
@@ -69,7 +69,7 @@
 
         marsh.setContext(new MarshallerContextTestImpl(null));
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
 
         return marsh;
     }
@@ -461,7 +461,7 @@
             if (val instanceof BinaryObject)
                 val = ((BinaryObject)val).deserialize();
 
-            if (val != null && val.getClass().isArray()) {
+            if (F.isArray(expVal)) {
                 assertNotNull(expVal);
 
                 if (val instanceof byte[])
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFooterOffsetsAbstractSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFooterOffsetsAbstractSelfTest.java
index 53211c5..c338cdd 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFooterOffsetsAbstractSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryFooterOffsetsAbstractSelfTest.java
@@ -22,7 +22,6 @@
 import org.apache.ignite.binary.BinaryTypeConfiguration;
 import org.apache.ignite.configuration.BinaryConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -64,7 +63,7 @@
 
         marsh.setContext(new MarshallerContextTestImpl(null));
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
     }
 
     /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryMarshallerSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryMarshallerSelfTest.java
index 014f364..4431e88 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryMarshallerSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryMarshallerSelfTest.java
@@ -89,7 +89,6 @@
 import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.platform.utils.PlatformUtils;
 import org.apache.ignite.internal.util.GridUnsafe;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.lang.GridMapEntry;
 import org.apache.ignite.internal.util.lang.IgnitePair;
 import org.apache.ignite.internal.util.lang.IgniteThrowableConsumer;
@@ -104,7 +103,6 @@
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.GridTestKernalContext;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Assert;
@@ -119,7 +117,7 @@
  * Binary marshaller tests.
  */
 @SuppressWarnings({"OverlyStrongTypeCast", "ConstantConditions"})
-public class BinaryMarshallerSelfTest extends GridCommonAbstractTest {
+public class BinaryMarshallerSelfTest extends AbstractBinaryArraysTest {
     /**
      * @throws Exception If failed.
      */
@@ -502,6 +500,33 @@
         assertArrayEquals(arr, marshalUnmarshal(arr));
     }
 
+    /** */
+    @Test
+    public void testBinaryArray() throws IgniteCheckedException {
+        TestClass1[] arr = new TestClass1[] {new TestClass1(), new TestClass1()};
+
+        assertArrayEquals(arr, marshalUnmarshal(arr));
+
+        Object[] arr1 = new Object[] {arr, arr};
+
+        Object[] arr2 = marshalUnmarshal(arr1);
+
+        Assert.assertSame("Same array should be returned because of HANDLE usage", arr2[0], arr2[1]);
+
+        assertArrayEquals(arr1, arr2);
+
+        ArrayFieldClass o1 = new ArrayFieldClass();
+
+        o1.arr1 = arr;
+        o1.arr2 = arr;
+
+        ArrayFieldClass o2 = marshalUnmarshal(o1);
+
+        Assert.assertSame("Same array should be returned because of HANDLE usage", o2.arr1, o2.arr2);
+
+        assertArrayEquals(o1.arr1, o2.arr1);
+    }
+
     /**
      * @throws Exception If failed.
      */
@@ -805,11 +830,14 @@
         assertArrayEquals(obj.strArr, (String[])po.field("strArr"));
         assertArrayEquals(obj.uuidArr, (UUID[])po.field("uuidArr"));
         assertArrayEquals(obj.dateArr, (Date[])po.field("dateArr"));
-        assertArrayEquals(obj.objArr, (Object[])po.field("objArr"));
+        assertArrayEquals(
+            obj.objArr,
+            useBinaryArrays ? po.<BinaryArray>field("objArr").array() : po.field("objArr")
+        );
         assertEquals(obj.col, po.field("col"));
         assertEquals(obj.map, po.field("map"));
         assertEquals(new Integer(obj.enumVal.ordinal()), new Integer(((BinaryObject)po.field("enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.enumArr), ordinals((BinaryObject[])po.field("enumArr")));
+        assertArrayEquals(ordinals(obj.enumArr), binaryOrdinals(po.field("enumArr")));
         assertNull(po.field("unknown"));
 
         BinaryObject innerPo = po.field("inner");
@@ -840,12 +868,15 @@
         assertArrayEquals(obj.inner.strArr, (String[])innerPo.field("strArr"));
         assertArrayEquals(obj.inner.uuidArr, (UUID[])innerPo.field("uuidArr"));
         assertArrayEquals(obj.inner.dateArr, (Date[])innerPo.field("dateArr"));
-        assertArrayEquals(obj.inner.objArr, (Object[])innerPo.field("objArr"));
+        assertArrayEquals(
+            obj.inner.objArr,
+            useBinaryArrays ? innerPo.<BinaryArray>field("objArr").array() : innerPo.field("objArr")
+        );
         assertEquals(obj.inner.col, innerPo.field("col"));
         assertEquals(obj.inner.map, innerPo.field("map"));
         assertEquals(new Integer(obj.inner.enumVal.ordinal()),
             new Integer(((BinaryObject)innerPo.field("enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.inner.enumArr), ordinals((BinaryObject[])innerPo.field("enumArr")));
+        assertArrayEquals(ordinals(obj.inner.enumArr), binaryOrdinals(innerPo.field("enumArr")));
         assertNull(innerPo.field("inner"));
         assertNull(innerPo.field("unknown"));
     }
@@ -889,11 +920,14 @@
         assertArrayEquals(obj.strArr, (String[])po.field("_strArr"));
         assertArrayEquals(obj.uuidArr, (UUID[])po.field("_uuidArr"));
         assertArrayEquals(obj.dateArr, (Date[])po.field("_dateArr"));
-        assertArrayEquals(obj.objArr, (Object[])po.field("_objArr"));
+        assertArrayEquals(
+            obj.objArr,
+            useBinaryArrays ? po.<BinaryArray>field("_objArr").array() : po.field("_objArr")
+        );
         assertEquals(obj.col, po.field("_col"));
         assertEquals(obj.map, po.field("_map"));
         assertEquals(new Integer(obj.enumVal.ordinal()), new Integer(((BinaryObject)po.field("_enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.enumArr), ordinals((BinaryObject[])po.field("_enumArr")));
+        assertArrayEquals(ordinals(obj.enumArr), binaryOrdinals(po.field("_enumArr")));
         assertNull(po.field("unknown"));
 
         BinaryObject simplePo = po.field("_simple");
@@ -924,12 +958,15 @@
         assertArrayEquals(obj.simple.strArr, (String[])simplePo.field("strArr"));
         assertArrayEquals(obj.simple.uuidArr, (UUID[])simplePo.field("uuidArr"));
         assertArrayEquals(obj.simple.dateArr, (Date[])simplePo.field("dateArr"));
-        assertArrayEquals(obj.simple.objArr, (Object[])simplePo.field("objArr"));
+        assertArrayEquals(
+            obj.simple.objArr,
+            useBinaryArrays ? simplePo.<BinaryArray>field("objArr").array() : simplePo.field("objArr")
+        );
         assertEquals(obj.simple.col, simplePo.field("col"));
         assertEquals(obj.simple.map, simplePo.field("map"));
         assertEquals(new Integer(obj.simple.enumVal.ordinal()),
             new Integer(((BinaryObject)simplePo.field("enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.simple.enumArr), ordinals((BinaryObject[])simplePo.field("enumArr")));
+        assertArrayEquals(ordinals(obj.simple.enumArr), binaryOrdinals(simplePo.field("enumArr")));
         assertNull(simplePo.field("simple"));
         assertNull(simplePo.field("binary"));
         assertNull(simplePo.field("unknown"));
@@ -961,12 +998,15 @@
         assertArrayEquals(obj.binary.strArr, (String[])binaryPo.field("_strArr"));
         assertArrayEquals(obj.binary.uuidArr, (UUID[])binaryPo.field("_uuidArr"));
         assertArrayEquals(obj.binary.dateArr, (Date[])binaryPo.field("_dateArr"));
-        assertArrayEquals(obj.binary.objArr, (Object[])binaryPo.field("_objArr"));
+        assertArrayEquals(
+            obj.binary.objArr,
+            useBinaryArrays ? binaryPo.<BinaryArray>field("_objArr").array() : binaryPo.field("_objArr")
+        );
         assertEquals(obj.binary.col, binaryPo.field("_col"));
         assertEquals(obj.binary.map, binaryPo.field("_map"));
         assertEquals(new Integer(obj.binary.enumVal.ordinal()),
             new Integer(((BinaryObject)binaryPo.field("_enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.binary.enumArr), ordinals((BinaryObject[])binaryPo.field("_enumArr")));
+        assertArrayEquals(ordinals(obj.binary.enumArr), binaryOrdinals(binaryPo.field("_enumArr")));
         assertNull(binaryPo.field("_simple"));
         assertNull(binaryPo.field("_binary"));
         assertNull(binaryPo.field("unknown"));
@@ -1075,7 +1115,9 @@
                     col = bob.getField(flds.get1());
                 }
 
-                assertSame(col, colHnd);
+                // Must be assertSame but now BinaryObjectBuilder doesn't support handle to collection.
+                // Now we check only that BinaryObjectBuilder#getField doesn't crash and returns valid collection.
+                assertEquals("Check: " + flds, col, colHnd);
             }
 
             bo = bob.build();
@@ -1266,11 +1308,14 @@
         assertArrayEquals(obj.strArr, (String[])po.field("strArr"));
         assertArrayEquals(obj.uuidArr, (UUID[])po.field("uuidArr"));
         assertArrayEquals(obj.dateArr, (Date[])po.field("dateArr"));
-        assertArrayEquals(obj.objArr, (Object[])po.field("objArr"));
+        assertArrayEquals(
+            obj.objArr,
+            useBinaryArrays ? po.<BinaryArray>field("objArr").array() : po.field("objArr")
+        );
         assertEquals(obj.col, po.field("col"));
         assertEquals(obj.map, po.field("map"));
         assertEquals(new Integer(obj.enumVal.ordinal()), new Integer(((BinaryObject)po.field("enumVal")).enumOrdinal()));
-        assertArrayEquals(ordinals(obj.enumArr), ordinals((BinaryObject[])po.field("enumArr")));
+        assertArrayEquals(ordinals(obj.enumArr), binaryOrdinals(po.field("enumArr")));
         assertNull(po.field("unknown"));
 
         assertEquals(obj, po.deserialize());
@@ -1659,7 +1704,7 @@
 
         BinaryObject po = marshal(obj, marsh);
 
-        Object[] arr0 = po.field("arr");
+        Object[] arr0 = useBinaryArrays ? po.<BinaryArray>field("arr").array() : po.field("arr");
 
         assertEquals(3, arr0.length);
 
@@ -2220,9 +2265,8 @@
         try {
             binaryMarshaller(Arrays.asList(customType1, customType2));
         }
-        catch (IgniteCheckedException e) {
-            assertEquals("Duplicate type ID [clsName=org.gridgain.Class2, id=100]",
-                e.getCause().getCause().getMessage());
+        catch (BinaryObjectException e) {
+            assertEquals("Duplicate type ID [clsName=org.gridgain.Class2, id=100]", e.getMessage());
 
             return;
         }
@@ -3928,14 +3972,27 @@
     }
 
     /**
-     * @param enumArr Enum array.
+     * @param fld Field value.
      * @return Ordinals.
      */
-    private <T extends Enum<?>> Integer[] ordinals(BinaryObject[] enumArr) {
+    private <T extends Enum<?>> Integer[] binaryOrdinals(Object fld) {
+        Object[] enumArr;
+
+        if (useBinaryArrays) {
+            assertTrue(fld instanceof BinaryEnumArray);
+
+            enumArr = ((BinaryEnumArray)fld).array();
+        }
+        else {
+            assertTrue(fld instanceof BinaryObject[]);
+
+            enumArr = (Object[])fld;
+        }
+
         Integer[] ords = new Integer[enumArr.length];
 
         for (int i = 0; i < enumArr.length; i++)
-            ords[i] = enumArr[i].enumOrdinal();
+            ords[i] = ((BinaryObject)enumArr[i]).enumOrdinal();
 
         return ords;
     }
@@ -4093,7 +4150,7 @@
 
         marsh.setContext(marshCtx);
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
 
         return marsh;
     }
@@ -4425,7 +4482,7 @@
     }
 
     /** */
-    private static class SimpleObject {
+    public static class SimpleObject {
         /** */
         private byte b;
 
@@ -5749,10 +5806,19 @@
         }
     }
 
+    /** */
+    public static class ArrayFieldClass {
+        /** */
+        protected TestClass1[] arr1;
+
+        /** */
+        protected TestClass1[] arr2;
+    }
+
     /**
      *
      */
-    private static class TestClass1 {
+    public static class TestClass1 {
         /** */
         private int intVal = 33;
 
@@ -5761,6 +5827,21 @@
 
         /** */
         private SimpleObject obj = TestClass0.constSimpleObject();
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o)
+                return true;
+            if (o == null || getClass() != o.getClass())
+                return false;
+            TestClass1 class1 = (TestClass1)o;
+            return intVal == class1.intVal && Objects.equals(strVal, class1.strVal) && Objects.equals(obj, class1.obj);
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Objects.hash(intVal, strVal, obj);
+        }
     }
 
     /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderAdditionalSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderAdditionalSelfTest.java
index c1a49d5..adc690f 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderAdditionalSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderAdditionalSelfTest.java
@@ -39,9 +39,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.BiConsumer;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -57,6 +57,7 @@
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.MarshallerPlatformIds;
+import org.apache.ignite.internal.binary.BinaryMarshallerSelfTest.TestClass1;
 import org.apache.ignite.internal.binary.builder.BinaryBuilderEnum;
 import org.apache.ignite.internal.binary.builder.BinaryObjectBuilderImpl;
 import org.apache.ignite.internal.binary.mutabletest.GridBinaryMarshalerAwareTestClass;
@@ -64,10 +65,10 @@
 import org.apache.ignite.internal.binary.test.GridBinaryTestClass2;
 import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
 import org.apache.ignite.internal.processors.cache.binary.IgniteBinaryImpl;
+import org.apache.ignite.internal.processors.platform.utils.PlatformUtils;
 import org.apache.ignite.internal.util.lang.GridMapEntry;
 import org.apache.ignite.marshaller.MarshallerContext;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -77,7 +78,7 @@
 /**
  *
  */
-public class BinaryObjectBuilderAdditionalSelfTest extends GridCommonAbstractTest {
+public class BinaryObjectBuilderAdditionalSelfTest extends AbstractBinaryArraysTest {
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
@@ -244,39 +245,6 @@
      *
      */
     @Test
-    public void testObjectHandleIsPossible() {
-        GridBinaryTestClasses.Address addr = new GridBinaryTestClasses.Address(
-                "Weligama", "Kapparatota", 203, 2
-        );
-
-        BinaryObjectBuilder objectBuilder = binaries().builder("customObject");
-
-        objectBuilder.setField("addr1", addr);
-        objectBuilder.setField("addr2", addr);
-
-        BinaryObject build = objectBuilder.build();
-
-        assertSame(build.field("addr1"), build.field("addr1"));
-        assertSame(build.field("addr1"), build.field("addr2"));
-
-        BinaryObjectBuilder secondBuilder = objectBuilder.build().toBuilder();
-
-        GridBinaryTestClasses.Address secondAddr = new GridBinaryTestClasses.Address(
-                "Tokyo", "Shibuya City", 122, 5
-        );
-
-        secondBuilder.setField("addr1", secondAddr);
-
-        BinaryObject build2 = secondBuilder.build();
-
-        assertSame(build2.field("addr1"), build2.field("addr1"));
-        assertNotSame(build2.field("addr1"), build2.field("addr2"));
-    }
-
-    /**
-     *
-     */
-    @Test
     public void testDateArrayModification() {
         GridBinaryTestClasses.TestObjectAllTypes obj = new GridBinaryTestClasses.TestObjectAllTypes();
 
@@ -869,44 +837,6 @@
      *
      */
     @Test
-    public void testCollectionsHandlePossible() {
-        GridBinaryTestClasses.Address testObject = new GridBinaryTestClasses.Address();
-        testObject.city = "city";
-        testObject.flatNumber = 1;
-        testObject.street = "street";
-        testObject.streetNumber = 32;
-
-        {
-            Collection<Object> list = Lists.newArrayList(testObject, 1, 2L, "a", "b");
-            testCollectionHandlePossible(list, Lists.newArrayList(list), testObject);
-        }
-
-        {
-            Collection<Object> list = Lists.newArrayList(testObject, 1, 2L, "a", "b");
-            Collection<Object> src = Lists.newLinkedList(list);
-
-            testCollectionHandlePossible(src, Lists.newLinkedList(src), testObject);
-        }
-
-        {
-            Collection<Object> list = Lists.newArrayList(testObject, 1, 2L, "a", "b");
-            Collection<Object> src = Sets.newHashSet(list);
-
-            testCollectionHandlePossible(src, Sets.newHashSet(src), testObject);
-        }
-
-        {
-            Collection<Object> list = Lists.newArrayList(testObject, 1, 2L, "a", "b");
-            Collection<Object> src = Sets.newLinkedHashSet(list);
-
-            testCollectionHandlePossible(src, Sets.newLinkedHashSet(src), testObject);
-        }
-    }
-
-    /**
-     *
-     */
-    @Test
     public void testMapRead() {
         GridBinaryTestClasses.TestObjectContainer obj = new GridBinaryTestClasses.TestObjectContainer();
         obj.foo = Maps.newHashMap(ImmutableMap.of(obj, "a", "b", obj));
@@ -962,29 +892,6 @@
      *
      */
     @Test
-    public void testMapsHandlePossible() {
-        GridBinaryTestClasses.Address testObject = new GridBinaryTestClasses.Address();
-        testObject.city = "city";
-        testObject.flatNumber = 1;
-        testObject.street = "street";
-        testObject.streetNumber = 32;
-
-        {
-            Map<Object, Object> src = Maps.newHashMap(ImmutableMap.of(1, "a", 2, "b", 3, testObject));
-            testMapHandlePossible(src, Maps.newHashMap(src), 3, testObject);
-        }
-
-        {
-            Map<Object, Object> src = Maps.newLinkedHashMap(ImmutableMap.of(1, "a", 2, "b", 3, testObject));
-
-            testMapHandlePossible(src, Maps.newLinkedHashMap(src), 3, testObject);
-        }
-    }
-
-    /**
-     *
-     */
-    @Test
     public void testEnumArrayModification() {
         GridBinaryTestClasses.TestObjectAllTypes obj = new GridBinaryTestClasses.TestObjectAllTypes();
 
@@ -1502,6 +1409,48 @@
     /**
      *
      */
+    @Test
+    public void testSameArray() {
+        GridBinaryTestClasses.TestObjectContainer obj = new GridBinaryTestClasses.TestObjectContainer();
+
+        Object[] arr1 = new Object[2];
+
+        arr1[0] = new Object[2];
+        arr1[0] = arr1[1];
+
+        obj.foo = arr1;
+
+        GridBinaryTestClasses.TestObjectContainer res = toBinary(obj).deserialize();
+
+        Object[] resArr = (Object[])res.foo;
+
+        assertSame(resArr[0], resArr[1]);
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testSameMultiDimensionalArray() {
+        GridBinaryTestClasses.TestObjectContainer obj = new GridBinaryTestClasses.TestObjectContainer();
+
+        Object[][] arr1 = new Object[1][2];
+
+        arr1[0][0] = new Object[2];
+        arr1[0][1] = arr1[0][0];
+
+        obj.foo = arr1;
+
+        GridBinaryTestClasses.TestObjectContainer res = toBinary(obj).deserialize();
+
+        Object[] resArr = (Object[])((Object[])res.foo)[0];
+
+        assertSame(resArr[0], resArr[1]);
+    }
+
+    /**
+     *
+     */
     @SuppressWarnings("TypeMayBeWeakened")
     @Test
     public void testCyclicArrayList() {
@@ -1704,14 +1653,20 @@
         BinaryObject extObj = builder.setField("extVal", exp).setField("extArr", expArr).build();
 
         assertEquals(exp, extObj.field("extVal"));
-        Assert.assertArrayEquals(expArr, (Object[])extObj.field("extArr"));
+        Assert.assertArrayEquals(
+            expArr,
+            useBinaryArrays ? extObj.<BinaryArray>field("extArr").array() : extObj.field("extArr")
+        );
 
         builder = extObj.toBuilder();
 
         extObj = builder.setField("intVal", 10).build();
 
         assertEquals(exp, extObj.field("extVal"));
-        Assert.assertArrayEquals(expArr, (Object[])extObj.field("extArr"));
+        Assert.assertArrayEquals(
+            expArr,
+            useBinaryArrays ? extObj.<BinaryArray>field("extArr").array() : extObj.field("extArr")
+        );
         assertEquals(Integer.valueOf(10), extObj.field("intVal"));
 
         builder = extObj.toBuilder();
@@ -1719,7 +1674,10 @@
         extObj = builder.setField("strVal", "some string").build();
 
         assertEquals(exp, extObj.field("extVal"));
-        Assert.assertArrayEquals(expArr, (Object[])extObj.field("extArr"));
+        Assert.assertArrayEquals(
+            expArr,
+            useBinaryArrays ? extObj.<BinaryArray>field("extArr").array() : extObj.field("extArr")
+        );
         assertEquals(Integer.valueOf(10), extObj.field("intVal"));
         assertEquals("some string", extObj.field("strVal"));
     }
@@ -1824,6 +1782,36 @@
         }
     }
 
+    /** */
+    @Test
+    public void testArray2() {
+        try {
+            TestClass1[] expArr = new TestClass1[] {new TestClass1()};
+
+            BiConsumer<TestClass1[], BinaryObject> checker = (arr, bobj) -> {
+                Object[] val = useBinaryArrays
+                    ? bobj.<BinaryArray>field("arr").deserialize()
+                    : PlatformUtils.unwrapBinariesInArray(bobj.field("arr"));
+
+                Assert.assertArrayEquals(arr, val);
+                Assert.assertArrayEquals(arr, ((TestClsWithArray)bobj.deserialize()).arr);
+            };
+
+            BinaryObjectBuilder builder = newWrapper(TestClsWithArray.class.getName());
+            BinaryObject arrObj = builder.setField("arr", expArr).build();
+            checker.accept(expArr, arrObj);
+
+            expArr = new TestClass1[] {new TestClass1(), new TestClass1()};
+
+            builder = newWrapper(arrObj.type().typeName());
+            arrObj = builder.setField("arr", expArr).build();
+            checker.accept(expArr, arrObj);
+        }
+        finally {
+            clearBinaryMeta();
+        }
+    }
+
     /**
      * Test {@link BinaryObjectBuilder#build()} adds type mapping to the binary marshaller's cache.
      */
@@ -1849,7 +1837,12 @@
      * @return Deserialized enums.
      */
     private TestEnum[] deserializeEnumBinaryArray(Object obj) {
-        Object[] arr = (Object[])obj;
+        if (useBinaryArrays)
+            return ((BinaryArray)obj).deserialize();
+
+        Object[] arr;
+
+        arr = (Object[])obj;
 
         final TestEnum[] res = new TestEnum[arr.length];
 
@@ -1887,93 +1880,30 @@
     }
 
     /**
-     *
+     * @throws Exception If fails
      */
-    private void testCollectionHandlePossible(Collection<Object> src, Collection<Object> modified, Object obj) {
-        GridBinaryTestClasses.CollectionsHolder listHolder = new GridBinaryTestClasses.CollectionsHolder();
+    @Test
+    public void testGetNotAssignedFieldInEmptyBuilder() {
+        BinaryObjectBuilder builder = binaries().builder("SomeType")
+                .setField("w", "wewe");
 
-        listHolder.firstCol = src;
-        listHolder.secondCol = src;
-        listHolder.obj = obj;
-
-        BinaryObjectBuilderImpl mutObj = wrap(listHolder);
-
-        assertSame(mutObj.getField("firstCol"), mutObj.getField("firstCol"));
-        assertSame(mutObj.getField("firstCol"), mutObj.getField("secondCol"));
-
-        GridBinaryTestClasses.CollectionsHolder deserialized = mutObj.build().deserialize();
-
-        assertEquals(deserialized.firstCol, deserialized.secondCol);
-        assertEquals(deserialized.firstCol, src);
-
-        Optional<Object> firstObj = deserialized.firstCol.stream()
-                .filter(e -> e instanceof GridBinaryTestClasses.Address).findFirst();
-        Optional<Object> secondObj = deserialized.secondCol.stream()
-                .filter(e -> e instanceof GridBinaryTestClasses.Address).findFirst();
-
-        assertSame(firstObj.get(), deserialized.obj);
-        assertSame(secondObj.get(), deserialized.obj);
-
-        mutObj.setField("firstCol", modified);
-
-        deserialized = mutObj.build().deserialize();
-
-        assertNotSame(deserialized.firstCol, deserialized.secondCol);
-        assertEquals(deserialized.firstCol, deserialized.secondCol);
-
-        firstObj = deserialized.firstCol.stream().filter(e -> e instanceof GridBinaryTestClasses.Address).findFirst();
-        secondObj = deserialized.secondCol.stream().filter(e -> e instanceof GridBinaryTestClasses.Address).findFirst();
-
-        assertEquals(firstObj.get(), deserialized.obj);
-        assertSame(secondObj.get(), deserialized.obj);
-    
-        BinaryObject anotherObject = wrap(listHolder).build();
-    
-        BinaryObjectBuilder bob = anotherObject.toBuilder().build().toBuilder().build().toBuilder();
-    
-        assertSame(bob.getField("firstCol"), bob.getField("firstCol"));
-        assertSame(bob.getField("firstCol"), bob.getField("secondCol"));
+        assertNull(builder.getField("field"));
+        assertEquals("wewe", builder.getField("w"));
     }
 
     /**
-     *
+     * @throws Exception If fails
      */
-    private void testMapHandlePossible(Map<Object, Object> src, Map<Object, Object> modified, Object key, Object obj) {
-        GridBinaryTestClasses.MapsHolder mapsHolder = new GridBinaryTestClasses.MapsHolder();
+    @Test
+    public void testGetNotAssignedFieldInBuilder() {
+        GridBinaryTestClasses.TestObjectContainer testObjectContainer = new GridBinaryTestClasses.TestObjectContainer();
+        testObjectContainer.foo = "binaryCachedValue";
+        BinaryObjectBuilderImpl builder = wrap(testObjectContainer);
+        builder.setField("w", "wewe");
 
-        mapsHolder.firstMap = src;
-        mapsHolder.secondMap = src;
-        mapsHolder.valObj = obj;
-
-        BinaryObjectBuilderImpl mutObj = wrap(mapsHolder);
-
-        assertSame(mutObj.getField("firstMap"), mutObj.getField("firstMap"));
-        assertSame(mutObj.getField("firstMap"), mutObj.getField("secondMap"));
-
-        GridBinaryTestClasses.MapsHolder deserialized = mutObj.build().deserialize();
-
-        assertEquals(deserialized.firstMap, deserialized.secondMap);
-        assertEquals(deserialized.firstMap, src);
-
-        assertSame(deserialized.firstMap.get(key), deserialized.valObj);
-        assertSame(deserialized.secondMap.get(key), deserialized.valObj);
-
-        mutObj.setField("firstMap", modified);
-
-        deserialized = mutObj.build().deserialize();
-
-        assertNotSame(deserialized.firstMap, deserialized.secondMap);
-        assertEquals(deserialized.firstMap, deserialized.secondMap);
-
-        assertEquals(deserialized.firstMap.get(key), deserialized.valObj);
-        assertSame(deserialized.secondMap.get(key), deserialized.valObj);
-        
-        BinaryObject anotherObject = wrap(mapsHolder).build();
-    
-        BinaryObjectBuilder bob = anotherObject.toBuilder().build().toBuilder().build().toBuilder();
-    
-        assertSame(bob.getField("firstMap"), bob.getField("firstMap"));
-        assertSame(bob.getField("firstMap"), bob.getField("secondMap"));
+        assertNull(builder.getField("field"));
+        assertEquals("wewe", builder.getField("w"));
+        assertEquals("binaryCachedValue", builder.getField("foo"));
     }
 
     /**
@@ -2050,6 +1980,17 @@
         }
     }
 
+    /** Test class with array. */
+    public static class TestClsWithArray {
+        /** */
+        private final Object[] arr;
+
+        /** */
+        public TestClsWithArray(TestClass1[] arr) {
+            this.arr = arr;
+        }
+    }
+
     /**
      *
      */
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderDefaultMappersSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderDefaultMappersSelfTest.java
index e58d969..c0449e4 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderDefaultMappersSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectBuilderDefaultMappersSelfTest.java
@@ -44,7 +44,6 @@
 import org.apache.ignite.internal.util.GridUnsafe;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -53,7 +52,7 @@
 /**
  * Binary builder test.
  */
-public class BinaryObjectBuilderDefaultMappersSelfTest extends GridCommonAbstractTest {
+public class BinaryObjectBuilderDefaultMappersSelfTest extends AbstractBinaryArraysTest {
     /** */
     private static IgniteConfiguration cfg;
 
@@ -632,7 +631,7 @@
         assertEquals(expectedHashCode("Class"), po.type().typeId());
         assertEquals(BinaryArrayIdentityResolver.instance().hashCode(po), po.hashCode());
 
-        Object[] arr = po.field("objectArrayField");
+        Object[] arr = useBinaryArrays ? po.<BinaryArray>field("objectArrayField").array() : po.field("objectArrayField");
 
         assertEquals(2, arr.length);
 
@@ -844,7 +843,9 @@
      */
     @Test
     public void testMetaData() throws Exception {
-        BinaryObjectBuilder builder = builder("org.test.MetaTest");
+        String cls = "org.test.MetaTest" + (useBinaryArrays ? "0" : "");
+
+        BinaryObjectBuilder builder = builder(cls);
 
         builder.setField("intField", 1);
         builder.setField("byteArrayField", new byte[] {1, 2, 3});
@@ -853,7 +854,7 @@
 
         BinaryType meta = po.type();
 
-        assertEquals(expectedTypeName("org.test.MetaTest"), meta.typeName());
+        assertEquals(expectedTypeName(cls), meta.typeName());
 
         Collection<String> fields = meta.fieldNames();
 
@@ -865,7 +866,7 @@
         assertEquals("int", meta.fieldTypeName("intField"));
         assertEquals("byte[]", meta.fieldTypeName("byteArrayField"));
 
-        builder = builder("org.test.MetaTest");
+        builder = builder(cls);
 
         builder.setField("intField", 2);
         builder.setField("uuidField", UUID.randomUUID());
@@ -874,7 +875,7 @@
 
         meta = po.type();
 
-        assertEquals(expectedTypeName("org.test.MetaTest"), meta.typeName());
+        assertEquals(expectedTypeName(cls), meta.typeName());
 
         fields = meta.fieldNames();
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java
index 5f74a03..b493050 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java
@@ -35,13 +35,12 @@
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.IgniteEx;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 import static java.util.Collections.singletonList;
 
 /** */
-public class BinaryObjectToStringTest extends GridCommonAbstractTest {
+public class BinaryObjectToStringTest extends AbstractBinaryArraysTest {
     /** */
     @Test
     public void testToStringInaccessibleOptimizedMarshallerClass() throws Exception {
@@ -57,7 +56,7 @@
         assertStringFormContains(new TestIntContainer(123), "i=123");
 
         assertStringFormContains(new TestContainer(new int[]{1, 2}), "x=[1, 2]");
-        assertStringFormContains(new TestContainer(new Integer[]{1, 2}), "x=[1, 2]");
+        assertStringFormContains(new TestContainer(new Integer[]{1, 2}), useBinaryArrays ? "[1, 2]" : "x=[1, 2]");
         assertStringFormContains(new TestContainer(new ArrayList<>(Arrays.asList(1, 2))), "x=ArrayList {1, 2}");
         assertStringFormContains(new TestContainer(new HashSet<>(Arrays.asList(1, 2))), "x=HashSet {1, 2}");
         assertStringFormContains(new TestContainer(new HashMap<>(ImmutableMap.of(1, 2))), "x=HashMap {1=2}");
@@ -228,4 +227,11 @@
             this.i = i;
         }
     }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryMarshallerCtxDisabledSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryMarshallerCtxDisabledSelfTest.java
index e7eb744..0c9c214 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryMarshallerCtxDisabledSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryMarshallerCtxDisabledSelfTest.java
@@ -28,7 +28,6 @@
 import org.apache.ignite.binary.BinaryWriter;
 import org.apache.ignite.binary.Binarylizable;
 import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.MarshallerContext;
@@ -52,7 +51,7 @@
 
         BinaryContext context = new BinaryContext(BinaryCachingMetadataHandler.create(), cfg, new NullLogger());
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", context, cfg);
+        marsh.setBinaryContext(context, cfg);
 
         SimpleObject simpleObj = new SimpleObject();
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryWildcardsSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryWildcardsSelfTest.java
index e5a3c08..26f0a6b 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryWildcardsSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/GridBinaryWildcardsSelfTest.java
@@ -32,7 +32,6 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.binary.test.GridBinaryTestClass1;
 import org.apache.ignite.internal.binary.test.GridBinaryTestClass2;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
@@ -681,7 +680,7 @@
 
         marsh.setContext(new MarshallerContextTestImpl(null));
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
 
         return marsh;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/mutabletest/GridBinaryTestClasses.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/mutabletest/GridBinaryTestClasses.java
index 4f446d9..067866c 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/binary/mutabletest/GridBinaryTestClasses.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/mutabletest/GridBinaryTestClasses.java
@@ -26,11 +26,9 @@
 import java.math.BigInteger;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.TreeMap;
 import java.util.UUID;
 import com.google.common.base.Throwables;
@@ -375,20 +373,6 @@
             this.streetNumber = streetNumber;
             this.flatNumber = flatNumber;
         }
-
-        /**  */
-        @Override public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            Address address = (Address)o;
-            return streetNumber == address.streetNumber && flatNumber == address.flatNumber
-                    && Objects.equals(city, address.city) && Objects.equals(street, address.street);
-        }
-
-        /**  */
-        @Override public int hashCode() {
-            return Objects.hash(city, street, streetNumber, flatNumber);
-        }
     }
 
     /**
@@ -511,32 +495,4 @@
             });
         }
     }
-
-    /**
-     *
-     */
-    public static class CollectionsHolder {
-        /** */
-        public Collection<Object> firstCol;
-
-        /** */
-        public Collection<Object> secondCol;
-
-        /** */
-        public Object obj;
-    }
-
-    /**
-     *
-     */
-    public static class MapsHolder {
-        /** */
-        public Map<Object, Object> firstMap;
-
-        /** */
-        public Map<Object, Object> secondMap;
-
-        /** */
-        public Object valObj;
-    }
 }
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesBinaryArraysTests.java
similarity index 73%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesBinaryArraysTests.java
index a5b6d8f..8e61f9c 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesBinaryArraysTests.java
@@ -15,18 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
+package org.apache.ignite.internal.client.thin;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-
-/**
- * Zookeeper IP Finder tests.
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
-
+/** */
+public class ServicesBinaryArraysTests extends ServicesTest {
+    /** */
+    public ServicesBinaryArraysTests() {
+        useBinaryArrays = true;
+    }
 }
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
index 31e83c3..ee670a1 100644
--- 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
@@ -27,15 +27,25 @@
 import org.apache.ignite.Ignite;
 import org.apache.ignite.client.ClientClusterGroup;
 import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.ClientServiceDescriptor;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.client.Person;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.platform.PlatformType;
 import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.resources.ServiceContextResource;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallContextBuilder;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
 
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
+import static org.junit.Assert.assertArrayEquals;
+
 /**
  * Checks service invocation for thin client.
  */
@@ -49,10 +59,16 @@
     /** Cluster-singleton service name. */
     private static final String CLUSTER_SINGLTON_SERVICE_NAME = "cluster_svc";
 
+    /** */
+    protected boolean useBinaryArrays;
+
     /** {@inheritDoc} */
     @Override protected void beforeTestsStarted() throws Exception {
         super.beforeTestsStarted();
 
+        System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(useBinaryArrays));
+        BinaryArray.initUseBinaryArrays();
+
         startGrids(3);
 
         startClientGrid(3);
@@ -72,6 +88,14 @@
             DEFAULT_CACHE_NAME, keyGrid1);
     }
 
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        super.afterTestsStopped();
+
+        System.clearProperty(IGNITE_USE_BINARY_ARRAYS);
+        BinaryArray.initUseBinaryArrays();
+    }
+
     /**
      * Test that overloaded methods resolved correctly.
      */
@@ -202,6 +226,39 @@
         }
     }
 
+    /** Test custom caller context. */
+    @Test
+    public void testServiceCallContext() {
+        String attrName = "testAttr";
+        String attrVal = "test";
+        String binAttrName = "binTestAttr";
+        byte[] binAttrVal = attrVal.getBytes();
+
+        try (IgniteClient client = startClient(0)) {
+            // Check proxy creation with an invalid implementation.
+            ServiceCallContext customCls = new ServiceCallContext() {
+                @Override public String attribute(String name) { return null; }
+
+                @Override public byte[] binaryAttribute(String name) { return null; }
+            };
+
+            GridTestUtils.assertThrowsAnyCause(log, () -> client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class, customCls), IllegalArgumentException.class, "\"callCtx\" has an invalid type.");
+
+            // Check proxy creation with a valid caller context.
+            ServiceCallContext callCtx = new ServiceCallContextBuilder()
+                .put(attrName, attrVal)
+                .put(binAttrName, binAttrVal)
+                .build();
+
+            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class, callCtx);
+
+            assertEquals(attrVal, svc.testContextAttribute(attrName));
+            assertArrayEquals(binAttrVal, svc.testContextBinaryAttribute(binAttrName));
+        }
+    }
+
     /**
      * Test that services executed on cluster group.
      */
@@ -267,6 +324,69 @@
         }
     }
 
+    /** Test service descriptors returned correctly. */
+    @Test
+    public void testServiceDescriptors() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            Collection<ClientServiceDescriptor> svcs = client.services().serviceDescriptors();
+
+            assertNotNull(svcs);
+
+            assertEquals(3, svcs.size());
+
+            assertTrue(svcs.stream().filter(svc -> svc.name().equals(NODE_ID_SERVICE_NAME)).peek(svc -> {
+                assertEquals(NODE_ID_SERVICE_NAME, svc.name());
+                assertEquals(TestNodeIdService.class.getName(), svc.serviceClass());
+                assertEquals(0, svc.totalCount());
+                assertEquals(1, svc.maxPerNodeCount());
+                assertNull(svc.cacheName());
+                assertEquals(grid(0).localNode().id(), svc.originNodeId());
+                assertEquals(PlatformType.JAVA, svc.platformType());
+
+                assertDescriptorsEquals(svc, client.services().serviceDescriptor(NODE_ID_SERVICE_NAME));
+            }).findFirst().isPresent());
+
+            assertTrue(svcs.stream().filter(svc -> svc.name().equals(NODE_SINGLTON_SERVICE_NAME)).peek(svc -> {
+                assertEquals(NODE_SINGLTON_SERVICE_NAME, svc.name());
+                assertEquals(TestService.class.getName(), svc.serviceClass());
+                assertEquals(0, svc.totalCount());
+                assertEquals(1, svc.maxPerNodeCount());
+                assertNull(svc.cacheName());
+                assertEquals(grid(0).localNode().id(), svc.originNodeId());
+                assertEquals(PlatformType.JAVA, svc.platformType());
+
+                assertDescriptorsEquals(svc, client.services().serviceDescriptor(NODE_SINGLTON_SERVICE_NAME));
+            }).findFirst().isPresent());
+
+            assertTrue(svcs.stream().filter(svc -> svc.name().equals(CLUSTER_SINGLTON_SERVICE_NAME)).peek(svc -> {
+                assertEquals(CLUSTER_SINGLTON_SERVICE_NAME, svc.name());
+                assertEquals(TestService.class.getName(), svc.serviceClass());
+                assertEquals(1, svc.totalCount());
+                assertEquals(1, svc.maxPerNodeCount());
+                assertEquals(DEFAULT_CACHE_NAME, svc.cacheName());
+                assertEquals(grid(0).localNode().id(), svc.originNodeId());
+                assertEquals(PlatformType.JAVA, svc.platformType());
+
+                assertDescriptorsEquals(svc, client.services().serviceDescriptor(CLUSTER_SINGLTON_SERVICE_NAME));
+            }).findFirst().isPresent());
+
+            assertThrowsWithCause(() -> {
+                client.services().serviceDescriptor("unknown");
+            }, ClientException.class);
+        }
+    }
+
+    /** */
+    private void assertDescriptorsEquals(ClientServiceDescriptor svc, ClientServiceDescriptor svc1) {
+        assertEquals(svc1.name(), svc.name());
+        assertEquals(svc1.serviceClass(), svc.serviceClass());
+        assertEquals(svc1.totalCount(), svc.totalCount());
+        assertEquals(svc1.maxPerNodeCount(), svc.maxPerNodeCount());
+        assertEquals(svc1.cacheName(), svc.cacheName());
+        assertEquals(svc1.originNodeId(), svc.originNodeId());
+        assertEquals(svc1.platformType(), svc.platformType());
+    }
+
     /** */
     public static interface TestServiceInterface {
         /** */
@@ -300,6 +420,12 @@
         public Object testException();
 
         /** */
+        public String testContextAttribute(String name);
+
+        /** */
+        public byte[] testContextBinaryAttribute(String name);
+
+        /** */
         public boolean waitLatch() throws Exception;
     }
 
@@ -310,20 +436,9 @@
         /** Latch. */
         public static CountDownLatch latch;
 
-        /** {@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.
-        }
+        /** Service context. */
+        @ServiceContextResource
+        private ServiceContext svcCtx;
 
         /** {@inheritDoc} */
         @Override public String testMethod() {
@@ -376,6 +491,20 @@
         }
 
         /** {@inheritDoc} */
+        @Override public String testContextAttribute(String name) {
+            ServiceCallContext callCtx = svcCtx.currentCallContext();
+
+            return callCtx == null ? null : callCtx.attribute(name);
+        }
+
+        /** {@inheritDoc} */
+        @Override public byte[] testContextBinaryAttribute(String name) {
+            ServiceCallContext callCtx = svcCtx.currentCallContext();
+
+            return callCtx == null ? null : callCtx.binaryAttribute(name);
+        }
+
+        /** {@inheritDoc} */
         @Override public boolean waitLatch() throws Exception {
             latch.await(10L, TimeUnit.SECONDS);
 
@@ -400,21 +529,6 @@
         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/internal/direct/DirectMarshallingMessagesTest.java b/modules/core/src/test/java/org/apache/ignite/internal/direct/DirectMarshallingMessagesTest.java
new file mode 100644
index 0000000..698f0ff
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/direct/DirectMarshallingMessagesTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.direct;
+
+import java.nio.ByteBuffer;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.ignite.internal.managers.communication.GridIoMessageFactory;
+import org.apache.ignite.internal.managers.communication.IgniteMessageFactoryImpl;
+import org.apache.ignite.internal.util.distributed.SingleNodeMessage;
+import org.apache.ignite.plugin.extensions.communication.IgniteMessageFactory;
+import org.apache.ignite.plugin.extensions.communication.Message;
+import org.apache.ignite.plugin.extensions.communication.MessageFactory;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.TEST_PROCESS;
+
+/**
+ * Messages marshalling test.
+ */
+public class DirectMarshallingMessagesTest extends GridCommonAbstractTest {
+    /** Protocol version. */
+    private static final byte PROTO_VER = 2;
+
+    /** Message factory. */
+    private final IgniteMessageFactory msgFactory =
+        new IgniteMessageFactoryImpl(new MessageFactory[] {new GridIoMessageFactory()});
+
+    /** */
+    @Test
+    public void testSingleNodeMessage() {
+        SingleNodeMessage<?> srcMsg =
+            new SingleNodeMessage<>(UUID.randomUUID(), TEST_PROCESS, "data", new Exception("error"));
+
+        SingleNodeMessage<?> resMsg = doMarshalUnmarshal(srcMsg);
+
+        assertEquals(srcMsg.type(), resMsg.type());
+        assertEquals(srcMsg.processId(), resMsg.processId());
+        assertEquals(srcMsg.response(), resMsg.response());
+        assertEquals(srcMsg.error().getClass(), resMsg.error().getClass());
+        assertEquals(srcMsg.error().getMessage(), resMsg.error().getMessage());
+    }
+
+    /**
+     * @param srcMsg Message to marshal.
+     * @param <T> Message type.
+     * @return Unmarshalled message.
+     */
+    private <T extends Message> T doMarshalUnmarshal(T srcMsg) {
+        ByteBuffer buf = ByteBuffer.allocate(8 * 1024);
+
+        boolean fullyWritten = loopBuffer(buf, 0, buf0 -> srcMsg.writeTo(buf0, new DirectMessageWriter(PROTO_VER)));
+        assertTrue("The message was not written completely.", fullyWritten);
+
+        buf.flip();
+
+        byte b0 = buf.get();
+        byte b1 = buf.get();
+
+        short type = (short)((b1 & 0xFF) << 8 | b0 & 0xFF);
+
+        assertEquals(srcMsg.directType(), type);
+
+        T resMsg = (T)msgFactory.create(type);
+
+        boolean fullyRead = loopBuffer(buf, buf.position(),
+            buf0 -> resMsg.readFrom(buf0, new DirectMessageReader(msgFactory, PROTO_VER)));
+        assertTrue("The message was not read completely.", fullyRead);
+
+        return resMsg;
+    }
+
+    /**
+     * @param buf Byte buffer.
+     * @param start Start position.
+     * @param func Function that is sequentially executed on a different-sized part of the buffer.
+     * @return {@code True} if the function returns {@code True} at least once, {@code False} otherwise.
+     */
+    private boolean loopBuffer(ByteBuffer buf, int start, Function<ByteBuffer, Boolean> func) {
+        int pos = start;
+
+        do {
+            buf.position(start);
+            buf.limit(++pos);
+
+            if (func.apply(buf))
+                return true;
+        }
+        while (pos < buf.capacity());
+
+        return false;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/managers/events/LifecycleAwareListenerTest.java b/modules/core/src/test/java/org/apache/ignite/internal/managers/events/LifecycleAwareListenerTest.java
new file mode 100644
index 0000000..79d2c66
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/managers/events/LifecycleAwareListenerTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.managers.events;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.events.Event;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.lang.IgnitePredicate;
+import org.apache.ignite.lifecycle.LifecycleAware;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.events.EventType.EVTS_ALL;
+
+/**
+ * Tests local event listener that implements {@link LifecycleAware}.
+ */
+public class LifecycleAwareListenerTest extends GridCommonAbstractTest {
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testStartStop() throws Exception {
+        TestLocalListener lsnr = new TestLocalListener();
+
+        IgniteConfiguration cfg = getConfiguration().setLocalEventListeners(F.asMap(lsnr, EVTS_ALL));
+
+        try (Ignite ignite = startGrid(cfg)) {
+            assertTrue(lsnr.isStarted);
+            assertFalse(lsnr.isStopped);
+        }
+
+        assertTrue(lsnr.isStopped);
+    }
+
+    /** */
+    private static class TestLocalListener implements IgnitePredicate<Event>, LifecycleAware {
+        /** Is started. */
+        private boolean isStarted;
+
+        /** Is stopped. */
+        private boolean isStopped;
+
+        /** {@inheritDoc} */
+        @Override public boolean apply(Event evt) {
+            return true;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void start() throws IgniteException {
+            assertFalse(isStarted);
+
+            isStarted = true;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void stop() throws IgniteException {
+            assertFalse(isStopped);
+
+            isStopped = true;
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/metric/JmxExporterSpiTest.java b/modules/core/src/test/java/org/apache/ignite/internal/metric/JmxExporterSpiTest.java
index 9b81560..923161f 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/metric/JmxExporterSpiTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/metric/JmxExporterSpiTest.java
@@ -771,7 +771,7 @@
         assertEquals(toStringSafe(ignite.context().discovery().topologyVersionEx()), view.get("topology"));
         assertEquals(TEST_TRANSFORMER, view.get("transformer"));
         assertFalse((Boolean)view.get("keepBinary"));
-        assertEquals("null", view.get("subjectId"));
+        assertNull(view.get("subjectId"));
         assertNull(view.get("taskName"));
 
         qryRes1.close();
@@ -923,7 +923,7 @@
             assertEquals(toStringSafe(client1.context().discovery().topologyVersionEx()), view.get("topology"));
             assertEquals(TEST_TRANSFORMER, view.get("transformer"));
             assertFalse((Boolean)view.get("keepBinary"));
-            assertEquals("null", view.get("subjectId"));
+            assertNull(view.get("subjectId"));
             assertNull(view.get("taskName"));
             assertEquals(10, view.get("pageSize"));
         };
@@ -944,7 +944,7 @@
             assertEquals(toStringSafe(client2.context().discovery().topologyVersionEx()), view.get("topology"));
             assertNull(view.get("transformer"));
             assertTrue((Boolean)view.get("keepBinary"));
-            assertEquals("null", view.get("subjectId"));
+            assertNull(view.get("subjectId"));
             assertNull(view.get("taskName"));
             assertEquals(20, view.get("pageSize"));
         };
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/metric/SystemViewSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/metric/SystemViewSelfTest.java
index c97eb62..5bad616 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/metric/SystemViewSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/metric/SystemViewSelfTest.java
@@ -20,6 +20,7 @@
 import java.lang.reflect.Field;
 import java.sql.Connection;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -35,6 +36,8 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import javax.cache.Cache;
+
+import com.google.common.collect.Lists;
 import org.apache.ignite.IgniteAtomicLong;
 import org.apache.ignite.IgniteAtomicReference;
 import org.apache.ignite.IgniteAtomicSequence;
@@ -109,6 +112,7 @@
 import org.apache.ignite.spi.systemview.view.PagesListView;
 import org.apache.ignite.spi.systemview.view.ScanQueryView;
 import org.apache.ignite.spi.systemview.view.ServiceView;
+import org.apache.ignite.spi.systemview.view.SnapshotView;
 import org.apache.ignite.spi.systemview.view.StripedExecutorTaskView;
 import org.apache.ignite.spi.systemview.view.SystemView;
 import org.apache.ignite.spi.systemview.view.TransactionView;
@@ -142,6 +146,7 @@
 import static org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl.BINARY_METADATA_VIEW;
 import static org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager.METASTORE_VIEW;
 import static org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager.DATA_REGION_PAGE_LIST_VIEW;
+import static org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage.METASTORAGE_CACHE_NAME;
 import static org.apache.ignite.internal.processors.cache.transactions.IgniteTxManager.TXS_MON_LIST;
 import static org.apache.ignite.internal.processors.cluster.GridClusterStateProcessor.BASELINE_NODES_SYS_VIEW;
 import static org.apache.ignite.internal.processors.cluster.GridClusterStateProcessor.BASELINE_NODE_ATTRIBUTES_SYS_VIEW;
@@ -167,6 +172,7 @@
 import static org.apache.ignite.internal.util.IgniteUtils.toStringSafe;
 import static org.apache.ignite.internal.util.lang.GridFunc.alwaysTrue;
 import static org.apache.ignite.internal.util.lang.GridFunc.identity;
+import static org.apache.ignite.spi.systemview.view.SnapshotView.SNAPSHOT_SYS_VIEW;
 import static org.apache.ignite.testframework.GridTestUtils.runAsync;
 import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
 import static org.apache.ignite.transactions.TransactionConcurrency.OPTIMISTIC;
@@ -2044,6 +2050,59 @@
         }
     }
 
+    /** */
+    @Test
+    public void testSnapshot() throws Exception {
+        cleanPersistenceDir();
+
+        String dfltCacheGrp = "testGroup";
+
+        String testSnap0 = "testSnap0";
+        String testSnap1 = "testSnap1";
+
+        try (IgniteEx ignite = startGrid(getConfiguration()
+            .setCacheConfiguration(new CacheConfiguration<>(DEFAULT_CACHE_NAME).setGroupName(dfltCacheGrp))
+            .setDataStorageConfiguration(
+                new DataStorageConfiguration().setDefaultDataRegionConfiguration(
+                    new DataRegionConfiguration().setName("pds").setPersistenceEnabled(true)
+                )))
+        ) {
+            ignite.cluster().state(ClusterState.ACTIVE);
+
+            ignite.cache(DEFAULT_CACHE_NAME).put(1, 1);
+
+            ignite.snapshot().createSnapshot(testSnap0).get();
+
+            SystemView<SnapshotView> views = ignite.context().systemView().view(SNAPSHOT_SYS_VIEW);
+
+            assertEquals(1, views.size());
+
+            SnapshotView view = F.first(views);
+
+            assertEquals(testSnap0, view.name());
+            assertEquals(ignite.localNode().consistentId().toString(), view.consistentId());
+
+            Collection<?> constIds = F.nodeConsistentIds(ignite.cluster().nodes());
+
+            assertEquals(F.concat(constIds, ","), view.baselineNodes());
+            assertEquals(F.concat(Arrays.asList(dfltCacheGrp, METASTORAGE_CACHE_NAME), ","), view.cacheGroups());
+
+            ignite.createCache("testCache");
+
+            ignite.snapshot().createSnapshot(testSnap1).get();
+
+            views = ignite.context().systemView().view(SNAPSHOT_SYS_VIEW);
+
+            assertEquals(2, views.size());
+
+            List<String> exp = Lists.newArrayList(testSnap0, testSnap1);
+
+            views.forEach(v -> assertTrue(exp.remove(v.name())));
+
+            assertTrue(exp.isEmpty());
+        }
+    }
+
     /** Test node filter. */
     public static class TestNodeFilter implements IgnitePredicate<ClusterNode> {
         /** {@inheritDoc} */
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/authentication/AuthenticationConfigurationClusterTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/authentication/AuthenticationConfigurationClusterTest.java
index 71f34f4..31d2844 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/authentication/AuthenticationConfigurationClusterTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/authentication/AuthenticationConfigurationClusterTest.java
@@ -30,6 +30,7 @@
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
 import static org.apache.ignite.internal.processors.authentication.AuthenticationProcessorSelfTest.authenticate;
 import static org.apache.ignite.internal.processors.security.NoOpIgniteSecurityProcessor.SECURITY_DISABLED_ERROR_MSG;
 import static org.apache.ignite.plugin.security.SecurityPermissionSetBuilder.ALLOW_ALL;
@@ -124,7 +125,7 @@
      * Checks that a new node cannot join a cluster with a different authentication enable state.
      *
      * @param client Is joining node client.
-     * @param authEnabled Whether authentication is enabled on joining node.
+     * @param authEnabled Whether authentication is enabled on server node, which accepts join of new node.
      * @throws Exception If failed.
      */
     private void checkNodeJoinFailed(boolean client, boolean authEnabled) throws Exception {
@@ -211,4 +212,23 @@
             IgniteCheckedException.class,
             "Invalid security configuration: both authentication is enabled and external security plugin is provided.");
     }
+
+    /**
+     * Tests that client node configured with authentication but without persistence could start and join the cluster.
+     */
+    @Test
+    public void testClientNodeWithoutPersistence() throws Exception {
+        startGrid(configuration(0, true, false))
+            .cluster().state(ACTIVE);
+
+        IgniteConfiguration clientCfg = configuration(1, true, true);
+
+        clientCfg.getDataStorageConfiguration()
+            .getDefaultDataRegionConfiguration()
+            .setPersistenceEnabled(false);
+
+        startGrid(clientCfg);
+
+        assertEquals("Unexpected cluster size", 2, grid(1).cluster().nodes().size());
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/AbstractDataTypesCoverageTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/AbstractDataTypesCoverageTest.java
index ac1917f..f1a5e44 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/AbstractDataTypesCoverageTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/AbstractDataTypesCoverageTest.java
@@ -45,6 +45,7 @@
 import org.apache.ignite.cache.eviction.fifo.FifoEvictionPolicyFactory;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -52,6 +53,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+import static org.apache.ignite.internal.binary.BinaryArray.DFLT_IGNITE_USE_BINARY_ARRAYS;
+
 /**
  * Abstract data types coverage  test.
  */
@@ -144,17 +148,21 @@
     @Parameterized.Parameter(8)
     public boolean persistenceEnabled;
 
+    /** */
+    @Parameterized.Parameter(9)
+    public boolean useBinaryArrays;
+
     /**
      * @return Test parameters.
      */
     @Parameterized.Parameters(name = "atomicityMode={1}, cacheMode={2}, ttlFactory={3}, backups={4}," +
-        " evictionFactory={5}, onheapCacheEnabled={6}, writeSyncMode={7}, persistenceEnabled={8}")
+        " evictionFactory={5}, onheapCacheEnabled={6}, writeSyncMode={7}, persistenceEnabled={8}, useBinaryArrays={9}")
     public static Collection parameters() {
         Set<Object[]> params = new HashSet<>();
 
         Object[] baseParamLine = {
             null, CacheAtomicityMode.ATOMIC, CacheMode.PARTITIONED, null, 2, null,
-            false, CacheWriteSynchronizationMode.FULL_SYNC, false};
+            false, CacheWriteSynchronizationMode.FULL_SYNC, false, DFLT_IGNITE_USE_BINARY_ARRAYS};
 
         Object[] paramLine = null;
 
@@ -226,6 +234,14 @@
             params.add(paramLine);
         }
 
+        for (boolean useTypedArrays : BOOLEANS) {
+            paramLine = Arrays.copyOf(baseParamLine, baseParamLine.length);
+
+            paramLine[9] = useTypedArrays;
+
+            params.add(paramLine);
+        }
+
         for (Object[] pLine : params)
             pLine[0] = UUID.randomUUID();
 
@@ -233,6 +249,22 @@
     }
 
     /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(useBinaryArrays));
+        BinaryArray.initUseBinaryArrays();
+
+        super.beforeTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        System.clearProperty(IGNITE_USE_BINARY_ARRAYS);
+        BinaryArray.initUseBinaryArrays();
+    }
+
+    /** {@inheritDoc} */
     @Override protected long getTestTimeout() {
         return 30_000;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheAffinityCallSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheAffinityCallSelfTest.java
index f75ff9c..e684564 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheAffinityCallSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheAffinityCallSelfTest.java
@@ -20,7 +20,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.ThreadLocalRandom;
-
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteCompute;
@@ -31,7 +30,6 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
-import org.apache.ignite.internal.IgniteKernal;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteCallable;
@@ -67,7 +65,7 @@
         if (igniteInstanceName.equals(getTestIgniteInstanceName(SRVS)))
             ((TcpDiscoverySpi)cfg.getDiscoverySpi()).setForceServerMode(true);
         else {
-            CacheConfiguration ccfg = defaultCacheConfiguration();
+            CacheConfiguration<?, ?> ccfg = defaultCacheConfiguration();
             ccfg.setName(CACHE_NAME);
             ccfg.setCacheMode(PARTITIONED);
             ccfg.setBackups(1);
@@ -138,6 +136,48 @@
     }
 
     /**
+     * @throws Exception if failed.
+     */
+    @Test
+    public void testAffinityCallMergedExchanges() throws Exception {
+        startGrids(SRVS);
+
+        final Integer key = 1;
+
+        final IgniteEx client = startClientGrid(SRVS);
+
+        assertTrue(client.configuration().isClientMode());
+        assertNull(client.context().cache().cache(CACHE_NAME));
+
+        try {
+            grid(0).context().cache().context().exchange().mergeExchangesTestWaitVersion(
+                new AffinityTopologyVersion(SRVS + 3, 0),
+                null
+            );
+
+            IgniteInternalFuture<IgniteEx> fut1 = GridTestUtils.runAsync(() -> startGrid(SRVS + 1));
+
+            assertTrue(GridTestUtils.waitForCondition(() -> client.context().cache().context()
+                .exchange().lastTopologyFuture()
+                .initialVersion().equals(new AffinityTopologyVersion(SRVS + 2, 0)), 5_000));
+
+            assertFalse(fut1.isDone());
+
+            // The future should not complete until second node is started.
+            IgniteInternalFuture<Object> fut2 = GridTestUtils.runAsync(() ->
+                client.compute().affinityCall(CACHE_NAME, key, new CheckCallable(key, null)));
+
+            startGrid(SRVS + 2);
+
+            fut1.get();
+            fut2.get();
+        }
+        finally {
+            stopAllGrids();
+        }
+    }
+
+    /**
      * @throws Exception If failed.
      */
     @Test
@@ -240,7 +280,7 @@
         private Ignite ignite;
 
         /** */
-        private AffinityTopologyVersion topVer;
+        private final AffinityTopologyVersion topVer;
 
         /**
          * @param key Key.
@@ -255,7 +295,7 @@
         @Override public Object call() throws IgniteCheckedException {
             if (topVer != null) {
                 GridCacheAffinityManager aff =
-                    ((IgniteKernal)ignite).context().cache().internalCache(CACHE_NAME).context().affinity();
+                    ((IgniteEx)ignite).context().cache().internalCache(CACHE_NAME).context().affinity();
 
                 ClusterNode loc = ignite.cluster().localNode();
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheTopologyValidatorProviderTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheTopologyValidatorProviderTest.java
new file mode 100644
index 0000000..1caad69
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/CacheTopologyValidatorProviderTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.cache;
+
+import java.util.Collection;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.TopologyValidator;
+import org.apache.ignite.internal.processors.cache.IgniteTopologyValidatorAbstractCacheTest.TestCacheTopologyValidatorPluginProvider;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.plugin.CacheTopologyValidatorProvider;
+import org.apache.ignite.plugin.PluginProvider;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+
+/** */
+public class CacheTopologyValidatorProviderTest extends GridCommonAbstractTest {
+    /** */
+    private IgniteConfiguration getConfiguration(
+        int idx,
+        boolean isPersistenceEnabled,
+        PluginProvider<?>... providers
+    ) throws Exception {
+        IgniteConfiguration cfg = getConfiguration(getTestIgniteInstanceName(idx));
+
+        cfg.setPluginProviders(providers);
+
+        if (isPersistenceEnabled) {
+            cfg.setDataStorageConfiguration(new DataStorageConfiguration()
+                .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+                    .setMaxSize(100 * (1 << 20))
+                    .setPersistenceEnabled(true)));
+        }
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        cleanPersistenceDir();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        stopAllGrids();
+    }
+
+    /** */
+    @Test
+    public void testTopologyValidatorProviderWithPersistence() throws Exception {
+        startGrid(getConfiguration(0, true, new TestPluginProvider("top-validator", 0)));
+
+        grid(0).cluster().state(ACTIVE);
+
+        awaitPartitionMapExchange();
+
+        grid(0).createCache(DEFAULT_CACHE_NAME);
+
+        checkCachePut(DEFAULT_CACHE_NAME, true);
+
+        stopAllGrids();
+
+        PluginProvider<?> pluginProvider = new TestPluginProvider("top-validator", 1);
+
+        startGrid(getConfiguration(0, true, pluginProvider));
+
+        grid(0).cluster().state(ACTIVE);
+
+        checkCachePut(DEFAULT_CACHE_NAME, false);
+
+        assertEquals(0, grid(0).cache(DEFAULT_CACHE_NAME).get(0));
+
+        startGrid(getConfiguration(1, true, pluginProvider));
+
+        checkCachePut(DEFAULT_CACHE_NAME, true);
+
+        stopAllGrids();
+
+        startGrid(getConfiguration(0, true));
+
+        grid(0).cluster().state(ACTIVE);
+
+        checkCachePut(DEFAULT_CACHE_NAME, true);
+    }
+
+    /** */
+    @Test
+    public void testCacheConfigurationValidatorAlongsidePluginValidators() throws Exception {
+        PluginProvider<?> firstPluginProvider = new TestPluginProvider("first-top-validator", 2);
+        PluginProvider<?> secondPluginProvider = new TestPluginProvider("second-top-validator", 3);
+
+        startGrid(getConfiguration(0, false, firstPluginProvider, secondPluginProvider));
+
+        grid(0).createCache(new CacheConfiguration<>(DEFAULT_CACHE_NAME)
+            .setTopologyValidator(new TestTopologyValidator()));
+
+        checkCachePut(DEFAULT_CACHE_NAME, false);
+
+        startGrid(getConfiguration(1, false, firstPluginProvider, secondPluginProvider));
+
+        checkCachePut(DEFAULT_CACHE_NAME, false);
+
+        startGrid(getConfiguration(2, false, firstPluginProvider, secondPluginProvider));
+
+        checkCachePut(DEFAULT_CACHE_NAME, false);
+
+        startGrid(getConfiguration(3, false, firstPluginProvider, secondPluginProvider));
+
+        checkCachePut(DEFAULT_CACHE_NAME, true);
+    }
+
+    /** */
+    private void checkCachePut(String cacheName, boolean isSuccessExpected) {
+        for (Ignite ignite : G.allGrids()) {
+            if (isSuccessExpected) {
+                ignite.cache(cacheName).put(0, 0);
+
+                assertEquals(0, grid(0).cache(cacheName).get(0));
+            }
+            else {
+                GridTestUtils.assertThrows(
+                    log,
+                    () -> {
+                        ignite.cache(cacheName).put(0, 0);
+
+                        return null;
+                    },
+                    CacheInvalidStateException.class,
+                    "Failed to perform cache operation"
+                );
+            }
+        }
+    }
+
+    /** */
+    private static class TestTopologyValidator implements TopologyValidator {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** {@inheritDoc} */
+        @Override public boolean validate(Collection<ClusterNode> nodes) {
+            return nodes.size() > 1;
+        }
+    }
+
+    /** */
+    private static class TestPluginProvider extends TestCacheTopologyValidatorPluginProvider {
+        /** */
+        private final String name;
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return name;
+        }
+
+        /** */
+        private TestPluginProvider(String name, int validationThreshold) {
+            super(new TestCacheTopologyValidatorProvider(validationThreshold));
+
+            this.name = name;
+        }
+
+        /** */
+        private static class TestCacheTopologyValidatorProvider implements CacheTopologyValidatorProvider {
+            /** */
+            private final int validationThreshold;
+
+            /** */
+            public TestCacheTopologyValidatorProvider(int validationThreshold) {
+                this.validationThreshold = validationThreshold;
+            }
+
+            /** {@inheritDoc} */
+            @Override public TopologyValidator topologyValidator(String cacheName) {
+                return nodes -> nodes.size() > validationThreshold;
+            }
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheVersionSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheVersionSelfTest.java
index 77df9f6..12918ad 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheVersionSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheVersionSelfTest.java
@@ -47,10 +47,18 @@
         ver = version(0x7FFFFFF, 15);
         assertEquals(0x7FFFFFF, ver.nodeOrder());
         assertEquals(15, ver.dataCenterId());
+        assertEquals(
+            ver.toString(),
+            "GridCacheVersion [topVer=0, order=0, nodeOrder=" + 0x7FFFFFF + ", dataCenterId=15]"
+        );
 
         ver = version(0x7FFFFFF, 31);
         assertEquals(0x7FFFFFF, ver.nodeOrder());
         assertEquals(31, ver.dataCenterId());
+        assertEquals(
+            ver.toString(),
+            "GridCacheVersion [topVer=0, order=0, nodeOrder=" + 0x7FFFFFF + ", dataCenterId=31]"
+        );
 
         // Check max dr ID with some topology versions.
         ver = version(11, 31);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheManyAsyncOperationsTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheManyAsyncOperationsTest.java
index 6981063..d209867 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheManyAsyncOperationsTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheManyAsyncOperationsTest.java
@@ -21,8 +21,13 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import javax.cache.processor.EntryProcessor;
+import javax.cache.processor.MutableEntry;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.configuration.CacheConfiguration;
@@ -99,4 +104,34 @@
             }
         }
     }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testInvokeAsyncWithKeepBinary() throws Exception {
+        try (Ignite client = startClientGrid(gridCount())) {
+            assertTrue(client.configuration().isClientMode());
+
+            IgniteCache<Integer, BinaryObject> cache = client.cache(DEFAULT_CACHE_NAME).withKeepBinary();
+
+            BinaryObject value = client.binary().builder("TEST").build();
+
+            cache.put(1, value);
+
+            // Start parallel operations to initiate operation retry.
+            List<IgniteFuture<Void>> futs = IntStream.range(0, 1000).parallel().mapToObj(i ->
+                cache.invokeAsync(1, new EntryProcessor<Integer, BinaryObject, Void>() {
+                    @Override public Void process(MutableEntry<Integer, BinaryObject> e, Object... args) {
+                        BinaryObject val = e.getValue();
+
+                        assertNotNull(val);
+
+                        return null;
+                    }
+                })).collect(Collectors.toList());
+
+            futs.forEach(f -> f.get());
+        }
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractCacheTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractCacheTest.java
index ee59493..d5ac52b 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractCacheTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractCacheTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.cache;
 
 import java.io.Serializable;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -30,13 +31,30 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.configuration.TopologyValidator;
 import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.plugin.AbstractTestPluginProvider;
+import org.apache.ignite.plugin.CacheTopologyValidatorProvider;
+import org.apache.ignite.plugin.ExtensionRegistry;
+import org.apache.ignite.plugin.PluginContext;
 import org.apache.ignite.transactions.Transaction;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 /**
  * Topology validator test.
  */
+@RunWith(Parameterized.class)
 public abstract class IgniteTopologyValidatorAbstractCacheTest extends IgniteCacheAbstractTest implements Serializable {
+    /** */
+    @Parameterized.Parameter()
+    public Boolean isPluginTopValidatorProvider;
+
+    /** */
+    @Parameterized.Parameters(name = "isPluginTopValidatorProvider={0}")
+    public static Iterable<Object[]> data() {
+        return Arrays.asList(new Object[] {true}, new Object[] {false});
+    }
+
     /** key-value used at test. */
     private static String KEY_VAL = "1";
 
@@ -48,7 +66,19 @@
 
     /** {@inheritDoc} */
     @Override protected final int gridCount() {
-        return 1;
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        stopAllGrids();
     }
 
     /** {@inheritDoc} */
@@ -66,20 +96,14 @@
 
             cfg.setCacheConfiguration(cCfg0, cCfg1, cCfg2);
 
-            for (CacheConfiguration cCfg : cfg.getCacheConfiguration()) {
-                if (cCfg.getName().equals(CACHE_NAME_1))
-                    cCfg.setTopologyValidator(new TopologyValidator() {
-                        @Override public boolean validate(Collection<ClusterNode> nodes) {
-                            return servers(nodes) == 2;
-                        }
-                    });
-                else if (cCfg.getName().equals(CACHE_NAME_2))
-                    cCfg.setTopologyValidator(new TopologyValidator() {
-                        @Override public boolean validate(Collection<ClusterNode> nodes) {
-                            return servers(nodes) >= 2;
-                        }
-                    });
+            TestCacheTopologyValidatorProvider topValidatorProvider = new TestCacheTopologyValidatorProvider();
+
+            if (!isPluginTopValidatorProvider) {
+                for (CacheConfiguration cCfg : cfg.getCacheConfiguration())
+                    cCfg.setTopologyValidator(topValidatorProvider.topologyValidator(cCfg.getName()));
             }
+            else
+                cfg.setPluginProviders(new TestCacheTopologyValidatorPluginProvider(topValidatorProvider));
         }
 
         return cfg;
@@ -238,6 +262,8 @@
      */
     @Test
     public void testTopologyValidator() throws Exception {
+        startGrid(0);
+
         putValid(DEFAULT_CACHE_NAME);
         remove(DEFAULT_CACHE_NAME);
 
@@ -281,4 +307,48 @@
         putValid(CACHE_NAME_2);
         remove(CACHE_NAME_2);
     }
+
+    /** */
+    public static class TestCacheTopologyValidatorPluginProvider extends AbstractTestPluginProvider {
+        /** */
+        private final CacheTopologyValidatorProvider provider;
+
+        /** */
+        public TestCacheTopologyValidatorPluginProvider(CacheTopologyValidatorProvider provider) {
+            this.provider = provider;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return "CacheTopologyValidatorProviderPlugin";
+        }
+
+        /** {@inheritDoc} */
+        @Override public void initExtensions(PluginContext ctx, ExtensionRegistry registry) {
+            registry.registerExtension(CacheTopologyValidatorProvider.class, provider);
+        }
+    }
+
+    /** */
+    private static class TestCacheTopologyValidatorProvider implements CacheTopologyValidatorProvider, Serializable {
+        /** {@inheritDoc} */
+        @Override public TopologyValidator topologyValidator(String cacheName) {
+            if (CACHE_NAME_1.equals(cacheName)) {
+                return new TopologyValidator() {
+                    @Override public boolean validate(Collection<ClusterNode> nodes) {
+                        return servers(nodes) == 2;
+                    }
+                };
+            }
+            else if (CACHE_NAME_2.equals(cacheName)) {
+                return new TopologyValidator() {
+                    @Override public boolean validate(Collection<ClusterNode> nodes) {
+                        return servers(nodes) >= 2;
+                    }
+                };
+            }
+            else
+                return null;
+        }
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheGroupsTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheGroupsTest.java
index 8bd5091..e270174 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheGroupsTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheGroupsTest.java
@@ -46,6 +46,8 @@
     /** {@inheritDoc} */
     @Test
     @Override public void testTopologyValidator() throws Exception {
+        startGrid(0);
+
         try (Transaction tx = grid(0).transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
             putInvalid(CACHE_NAME_1);
         }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheTest.java
index e53b5a2..0fa3aa4 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorAbstractTxCacheTest.java
@@ -45,6 +45,8 @@
     /** {@inheritDoc} */
     @Test
     @Override public void testTopologyValidator() throws Exception {
+        startGrid(0);
+
         try (Transaction tx = grid(0).transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
             putInvalid(CACHE_NAME_1);
         }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorCacheGroupsAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorCacheGroupsAbstractTest.java
index a152e66..7392e93 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorCacheGroupsAbstractTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteTopologyValidatorCacheGroupsAbstractTest.java
@@ -17,12 +17,14 @@
 
 package org.apache.ignite.internal.processors.cache;
 
+import java.io.Serializable;
 import java.util.Collection;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.configuration.TopologyValidator;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.plugin.CacheTopologyValidatorProvider;
 import org.junit.Test;
 
 /**
@@ -45,36 +47,29 @@
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         IgniteConfiguration icfg = super.getConfiguration(igniteInstanceName);
 
-        CacheConfiguration[] ccfgs = icfg.getCacheConfiguration();
-
-        TopologyValidator val1 = new TopologyValidator() {
-            @Override public boolean validate(Collection<ClusterNode> nodes) {
-                return nodes.size() == 2;
-            }
-        };
-
-        TopologyValidator val2 = new TopologyValidator() {
-            @Override public boolean validate(Collection<ClusterNode> nodes) {
-                return nodes.size() >= 2;
-            }
-        };
+        CacheConfiguration[] ccfgs = F.concat(
+            icfg.getCacheConfiguration(),
+            cacheConfiguration(igniteInstanceName).setName(CACHE_NAME_3),
+            cacheConfiguration(igniteInstanceName).setName(CACHE_NAME_4)
+        );
 
         for (CacheConfiguration ccfg : ccfgs) {
             if (CACHE_NAME_1.equals(ccfg.getName()) || CACHE_NAME_2.equals(ccfg.getName()))
-                ccfg.setGroupName(GROUP_1).setTopologyValidator(val1);
+                ccfg.setGroupName(GROUP_1);
+            else if (CACHE_NAME_3.equals(ccfg.getName()) || CACHE_NAME_4.equals(ccfg.getName()))
+                ccfg.setGroupName(GROUP_2);
         }
 
-        CacheConfiguration ccfg3 = cacheConfiguration(igniteInstanceName)
-            .setName(CACHE_NAME_3)
-            .setGroupName(GROUP_2)
-            .setTopologyValidator(val2);
+        TestCacheGroupTopologyValidatorProvider topValidatorProvider = new TestCacheGroupTopologyValidatorProvider();
 
-        CacheConfiguration ccfg4 = cacheConfiguration(igniteInstanceName)
-            .setName(CACHE_NAME_4)
-            .setGroupName(GROUP_2)
-            .setTopologyValidator(val2);
+        if (isPluginTopValidatorProvider)
+            icfg.setPluginProviders(new TestCacheTopologyValidatorPluginProvider(topValidatorProvider));
+        else {
+            for (CacheConfiguration ccfg : ccfgs)
+                ccfg.setTopologyValidator(topValidatorProvider.topologyValidator(ccfg.getGroupName()));
+        }
 
-        return icfg.setCacheConfiguration(F.concat(ccfgs, ccfg3, ccfg4));
+        return icfg.setCacheConfiguration(ccfgs);
     }
 
     /**
@@ -82,6 +77,8 @@
      */
     @Test
     @Override public void testTopologyValidator() throws Exception {
+        startGrid(0);
+
         putValid(DEFAULT_CACHE_NAME);
         remove(DEFAULT_CACHE_NAME);
 
@@ -130,4 +127,27 @@
         putValid(CACHE_NAME_4);
         remove(CACHE_NAME_4);
     }
+
+    /** */
+    private static class TestCacheGroupTopologyValidatorProvider implements CacheTopologyValidatorProvider, Serializable {
+        /** {@inheritDoc} */
+        @Override public TopologyValidator topologyValidator(String grpName) {
+            if (GROUP_1.equals(grpName)) {
+                return new TopologyValidator() {
+                    @Override public boolean validate(Collection<ClusterNode> nodes) {
+                        return nodes.size() == 2;
+                    }
+                };
+            }
+            else if (GROUP_2.equals(grpName)) {
+                return new TopologyValidator() {
+                    @Override public boolean validate(Collection<ClusterNode> nodes) {
+                        return nodes.size() >= 2;
+                    }
+                };
+            }
+            else
+                return null;
+        }
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/GridBinaryCacheEntryMemorySizeSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/GridBinaryCacheEntryMemorySizeSelfTest.java
index 0e59aa8..a41b699 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/GridBinaryCacheEntryMemorySizeSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/GridBinaryCacheEntryMemorySizeSelfTest.java
@@ -27,7 +27,6 @@
 import org.apache.ignite.internal.managers.systemview.GridSystemViewManager;
 import org.apache.ignite.internal.managers.systemview.JmxSystemViewExporterSpi;
 import org.apache.ignite.internal.processors.cache.GridCacheEntryMemorySizeSelfTest;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
@@ -68,7 +67,7 @@
 
         BinaryContext pCtx = new BinaryContext(BinaryNoopMetadataHandler.instance(), iCfg, new NullLogger());
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", pCtx, iCfg);
+        marsh.setBinaryContext(pCtx, iCfg);
 
         return marsh;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractFullSetReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractFullSetReadRepairTest.java
index 01c0446..9897c30 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractFullSetReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractFullSetReadRepairTest.java
@@ -18,12 +18,18 @@
 package org.apache.ignite.internal.processors.cache.consistency;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.cache.CacheEntry;
+import org.apache.ignite.cache.ReadRepairStrategy;
+import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteIrreparableConsistencyViolationException;
 import org.apache.ignite.internal.util.typedef.G;
 import org.junit.Test;
 
@@ -39,24 +45,29 @@
         Set<Integer> keys = data.data.keySet();
         boolean raw = data.raw;
         boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
 
         assert keys.size() == 1;
 
         for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) { // Once.
             Integer key = entry.getKey();
-            Integer latest = entry.getValue().latest;
+            Integer fixed = entry.getValue().fixed;
 
-            Integer res =
-                raw ?
-                    async ?
-                        cache.withReadRepair().getEntryAsync(key).get().getValue() :
-                        cache.withReadRepair().getEntry(key).getValue() :
-                    async ?
-                        cache.withReadRepair().getAsync(key).get() :
-                        cache.withReadRepair().get(key);
+            Integer res;
 
-            if (latest != null)
-                assertEquals(latest, res);
+            if (raw) {
+                CacheEntry<Integer, Integer> rawEntry = async ?
+                    cache.withReadRepair(strategy).getEntryAsync(key).get() :
+                    cache.withReadRepair(strategy).getEntry(key);
+
+                res = rawEntry != null ? rawEntry.getValue() : null;
+            }
+            else
+                res = async ?
+                    cache.withReadRepair(strategy).getAsync(key).get() :
+                    cache.withReadRepair(strategy).get(key);
+
+            assertEquals(fixed, res);
         }
     };
 
@@ -68,33 +79,32 @@
         Set<Integer> keys = data.data.keySet();
         boolean raw = data.raw;
         boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
 
         assert !keys.isEmpty();
 
         if (raw) {
             Collection<CacheEntry<Integer, Integer>> res =
                 async ?
-                    cache.withReadRepair().getEntriesAsync(keys).get() :
-                    cache.withReadRepair().getEntries(keys);
+                    cache.withReadRepair(strategy).getEntriesAsync(keys).get() :
+                    cache.withReadRepair(strategy).getEntries(keys);
 
             for (CacheEntry<Integer, Integer> entry : res) {
-                Integer latest = data.data.get(entry.getKey()).latest;
+                Integer fixed = data.data.get(entry.getKey()).fixed;
 
-                if (latest != null)
-                    assertEquals(latest, entry.getValue());
+                assertEquals(fixed, entry.getValue());
             }
         }
         else {
             Map<Integer, Integer> res =
                 async ?
-                    cache.withReadRepair().getAllAsync(keys).get() :
-                    cache.withReadRepair().getAll(keys);
+                    cache.withReadRepair(strategy).getAllAsync(keys).get() :
+                    cache.withReadRepair(strategy).getAll(keys);
 
             for (Map.Entry<Integer, Integer> entry : res.entrySet()) {
-                Integer latest = data.data.get(entry.getKey()).latest;
+                Integer fixed = data.data.get(entry.getKey()).fixed;
 
-                if (latest != null)
-                    assertEquals(latest, entry.getValue());
+                assertEquals(fixed, entry.getValue());
             }
         }
     };
@@ -107,20 +117,21 @@
         Set<Integer> keys = data.data.keySet();
         boolean raw = data.raw;
         boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
 
         assert keys.size() == 1;
 
         for (Integer key : data.data.keySet()) { // Once.
-            Integer missed = key * -1; // Negative to gain null.
+            Integer missed = -key; // Negative to gain null.
 
             Object res =
                 raw ?
                     async ?
-                        cache.withReadRepair().getEntryAsync(missed).get() :
-                        cache.withReadRepair().getEntry(missed) :
+                        cache.withReadRepair(strategy).getEntryAsync(missed).get() :
+                        cache.withReadRepair(strategy).getEntry(missed) :
                     async ?
-                        cache.withReadRepair().getAsync(missed).get() :
-                        cache.withReadRepair().get(missed);
+                        cache.withReadRepair(strategy).getAsync(missed).get() :
+                        cache.withReadRepair(strategy).get(missed);
 
             assertEquals(null, res);
         }
@@ -129,19 +140,53 @@
     /**
      *
      */
+    protected static final Consumer<ReadRepairData> GET_ALL_NULL = (data) -> {
+        IgniteCache<Integer, Integer> cache = data.cache;
+        Set<Integer> keys = data.data.keySet();
+        boolean raw = data.raw;
+        boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
+
+        Set<Integer> missed = keys.stream().map(key -> -key).collect(Collectors.toCollection(TreeSet::new)); // Negative to gain null.
+
+        if (raw) {
+            Collection<CacheEntry<Integer, Integer>> res =
+                async ?
+                    cache.withReadRepair(strategy).getEntriesAsync(missed).get() :
+                    cache.withReadRepair(strategy).getEntries(missed);
+
+            assertTrue(res.isEmpty());
+        }
+        else {
+            Map<Integer, Integer> res =
+                async ?
+                    cache.withReadRepair(strategy).getAllAsync(missed).get() :
+                    cache.withReadRepair(strategy).getAll(missed);
+
+            assertTrue(res.isEmpty());
+        }
+    };
+
+    /**
+     *
+     */
     protected static final Consumer<ReadRepairData> CONTAINS_CHECK_AND_FIX = (data) -> {
         IgniteCache<Integer, Integer> cache = data.cache;
         Set<Integer> keys = data.data.keySet();
         boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
 
         assert keys.size() == 1;
 
-        for (Integer key : data.data.keySet()) { // Once.
-            boolean res = async ?
-                cache.withReadRepair().containsKeyAsync(key).get() :
-                cache.withReadRepair().containsKey(key);
+        for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) { // Once.
+            Integer key = entry.getKey();
+            Integer fixed = entry.getValue().fixed;
 
-            assertEquals(true, res);
+            boolean res = async ?
+                cache.withReadRepair(strategy).containsKeyAsync(key).get() :
+                cache.withReadRepair(strategy).containsKey(key);
+
+            assertEquals(fixed != null, res);
         }
     };
 
@@ -152,45 +197,103 @@
         IgniteCache<Integer, Integer> cache = data.cache;
         Set<Integer> keys = data.data.keySet();
         boolean async = data.async;
+        ReadRepairStrategy strategy = data.strategy;
 
         boolean res = async ?
-            cache.withReadRepair().containsKeysAsync(keys).get() :
-            cache.withReadRepair().containsKeys(keys);
+            cache.withReadRepair(strategy).containsKeysAsync(keys).get() :
+            cache.withReadRepair(strategy).containsKeys(keys);
 
-        assertEquals(true, res);
+        boolean allFixed = true;
+
+        for (Integer key : keys) {
+            Integer fixed = data.data.get(key).fixed;
+
+            if (fixed == null)
+                allFixed = false;
+        }
+
+        assertEquals(allFixed, res);
     };
 
     /**
      *
      */
-    protected static final Consumer<ReadRepairData> CHECK_FIXED = (data) -> {
-        IgniteCache<Integer, Integer> cache = data.cache;
-        boolean raw = data.raw;
+    protected static final BiConsumer<ReadRepairData, IgniteIrreparableConsistencyViolationException> CHECK_FIXED =
+        (data, e) -> {
+            IgniteCache<Integer, Integer> cache = data.cache;
+            boolean raw = data.raw;
 
-        for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) {
-            Integer key = entry.getKey();
-            Integer latest = entry.getValue().latest;
+            for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) {
+                Integer key = entry.getKey();
 
-            Integer res = raw ?
-                cache.getEntry(key).getValue() :
-                cache.get(key);
+                // Checking only fixed entries, while entries listed at exception were not fixed.
+                if (e != null && (e.irreparableKeys().contains(key) ||
+                    (e.repairableKeys() != null && e.repairableKeys().contains(key))))
+                    continue;
 
-            if (latest != null)
-                assertEquals(latest, res);
+                Integer fixed = entry.getValue().fixed;
+
+                Integer res;
+
+                if (raw) {
+                    CacheEntry<Integer, Integer> rawEntry = cache.getEntry(key);
+
+                    res = rawEntry != null ? rawEntry.getValue() : null;
+                }
+                else
+                    res = cache.get(key);
+
+                assertEquals(fixed, res);
+            }
+        };
+
+    /**
+     *
+     */
+    protected final BiConsumer<ReadRepairData, Runnable> repairIfRepairable = (data, r) -> {
+        try {
+            r.run();
+
+            assertTrue(data.repairable());
+        }
+        catch (Exception e) {
+            Throwable cause = e.getCause();
+
+            assertTrue(cause instanceof IgniteIrreparableConsistencyViolationException);
+            assertFalse(data.repairable());
+
+            check(data, (IgniteIrreparableConsistencyViolationException)cause, true);
         }
     };
 
     /**
      * @param data Data.
      */
-    protected void check(ReadRepairData data, boolean checkEvtRecorded, boolean checkFixed) {
-        if (checkEvtRecorded)
-            checkEvent(data, checkFixed);
+    protected void check(ReadRepairData data, IgniteIrreparableConsistencyViolationException e, boolean evtRecorded) {
+        Collection<?> irreparableKeys = e != null ? e.irreparableKeys() : null;
+
+        if (e != null) {
+            Collection<?> repairableKeys = e.repairableKeys();
+
+            if (repairableKeys != null)
+                assertTrue(Collections.disjoint(repairableKeys, irreparableKeys));
+
+            Collection<?> expectedToBeIrreparableKeys = data.data.entrySet().stream()
+                .filter(entry -> !entry.getValue().repairable)
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toSet());
+
+            assertEquals(expectedToBeIrreparableKeys, irreparableKeys);
+        }
+
+        assertEquals(irreparableKeys == null, data.repairable());
+
+        if (evtRecorded)
+            checkEvent(data, e);
         else
             checkEventMissed();
 
-        if (checkFixed)
-            CHECK_FIXED.accept(data);
+        CHECK_FIXED.accept(data, e);
     }
 
     /**
@@ -198,35 +301,23 @@
      */
     @Test
     public void test() throws Exception {
-        for (Ignite node : G.allGrids()) {
-            testGetVariations(node);
-            testGetNull(node);
-            testContainsVariations(node);
+        for (Ignite initiator : G.allGrids()) {
+            test(initiator, 1, false); // just get
+            test(initiator, 1, true); // 1 (all keys available at primary)
+            test(initiator, 2, true); // less than backups
+            test(initiator, 3, true); // equals to backups
+            test(initiator, 4, true); // equals to backups + primary
+            test(initiator, 10, true); // more than backups + primary
         }
     }
 
     /**
      *
      */
-    private void testGetVariations(Ignite initiator) throws Exception {
-        testGet(initiator, 1, false); // just get
-        testGet(initiator, 1, true); // 1 (all keys available at primary)
-        testGet(initiator, 2, true); // less than backups
-        testGet(initiator, 3, true); // equals to backups
-        testGet(initiator, 4, true); // equals to backups + primary
-        testGet(initiator, 10, true); // more than backups
-    }
-
-    /**
-     *
-     */
-    private void testContainsVariations(Ignite initiator) throws Exception {
-        testContains(initiator, 1, false); // just contains
-        testContains(initiator, 1, true); // 1 (all keys available at primary)
-        testContains(initiator, 2, true); // less than backups
-        testContains(initiator, 3, true); // equals to backups
-        testContains(initiator, 4, true); // equals to backups + primary
-        testContains(initiator, 10, true); // more than backups
+    private void test(Ignite initiator, Integer cnt, boolean all) throws Exception {
+        testGet(initiator, cnt, all);
+        testGetNull(initiator, cnt, all);
+        testContains(initiator, cnt, all);
     }
 
     /**
@@ -237,7 +328,7 @@
     /**
      *
      */
-    protected abstract void testGetNull(Ignite initiator) throws Exception;
+    protected abstract void testGetNull(Ignite initiator, Integer cnt, boolean all) throws Exception;
 
     /**
      *
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractReadRepairTest.java
index 7ded65a..72358b4 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AbstractReadRepairTest.java
@@ -18,26 +18,33 @@
 package org.apache.ignite.internal.processors.cache.consistency;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteEvents;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.events.CacheConsistencyViolationEvent;
 import org.apache.ignite.events.Event;
 import org.apache.ignite.events.EventType;
+import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.CacheObjectImpl;
@@ -45,18 +52,20 @@
 import org.apache.ignite.internal.processors.cache.GridCacheEntryEx;
 import org.apache.ignite.internal.processors.cache.GridCacheOperation;
 import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
+import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteIrreparableConsistencyViolationException;
 import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
 import org.apache.ignite.internal.processors.cache.version.GridCacheVersionManager;
 import org.apache.ignite.internal.processors.dr.GridDrType;
 import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
-import org.junit.Before;
 
 import static org.apache.ignite.cache.CacheAtomicityMode.ATOMIC;
 import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
 import static org.apache.ignite.cache.CacheMode.PARTITIONED;
+import static org.apache.ignite.cache.CacheMode.REPLICATED;
 import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC;
 import static org.apache.ignite.events.EventType.EVT_CONSISTENCY_VIOLATION;
 
@@ -81,17 +90,28 @@
     }
 
     /** Atomicy mode. */
-    protected CacheAtomicityMode atomicyMode() {
+    protected CacheAtomicityMode atomicityMode() {
         return TRANSACTIONAL;
     }
 
+    /** Persistence enabled. */
+    protected boolean persistenceEnabled() {
+        return false;
+    }
+
+    /** Server nodes count. */
+    private int serverNodesCount() {
+        return backupsCount() + 1/*primary*/ + 1/*not an owner*/;
+    }
+
     /** {@inheritDoc} */
     @Override protected void beforeTestsStarted() throws Exception {
         super.beforeTestsStarted();
 
-        Ignite ignite = startGrids(7); // Server nodes.
+        if (persistenceEnabled())
+            cleanPersistenceDir();
 
-        grid(0).getOrCreateCache(cacheConfiguration());
+        Ignite ignite = startGrids(serverNodesCount()); // Server nodes.
 
         startClientGrid(G.allGrids().size() + 1); // Client node 1.
         startClientGrid(G.allGrids().size() + 1); // Client node 2.
@@ -108,15 +128,12 @@
             },
             EVT_CONSISTENCY_VIOLATION);
 
-        awaitPartitionMapExchange();
-    }
+        if (persistenceEnabled())
+            ignite.cluster().state(ClusterState.ACTIVE);
 
-    /**
-     *
-     */
-    @Before
-    public void before() {
-        evtDeq.clear();
+        ignite.getOrCreateCache(cacheConfiguration());
+
+        awaitPartitionMapExchange();
     }
 
     /** {@inheritDoc} */
@@ -126,6 +143,9 @@
         log.info("Checked " + iterableKey + " keys");
 
         stopAllGrids();
+
+        if (persistenceEnabled())
+            cleanPersistenceDir();
     }
 
     /**
@@ -136,9 +156,11 @@
 
         cfg.setWriteSynchronizationMode(FULL_SYNC);
         cfg.setCacheMode(cacheMode());
-        cfg.setAtomicityMode(atomicyMode());
+        cfg.setAtomicityMode(atomicityMode());
         cfg.setBackups(backupsCount());
 
+        cfg.setAffinity(new RendezvousAffinityFunction().setPartitions(32));
+
         return cfg;
     }
 
@@ -148,6 +170,12 @@
 
         cfg.setIncludeEventTypes(EventType.EVTS_ALL);
 
+        if (persistenceEnabled()) {
+            cfg.setDataStorageConfiguration(new DataStorageConfiguration());
+            cfg.getDataStorageConfiguration().getDefaultDataRegionConfiguration().setMaxSize(100 * 1024 * 1024);
+            cfg.getDataStorageConfiguration().getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        }
+
         return cfg;
     }
 
@@ -161,49 +189,65 @@
     /**
      *
      */
-    protected void checkEvent(ReadRepairData data, boolean checkFixed) {
-        Map<Object, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> entries = new HashMap<>();
+    protected void checkEvent(ReadRepairData data, IgniteIrreparableConsistencyViolationException e) {
+        Map<Object, Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo>> evtEntries = new HashMap<>();
+        Map<Object, Object> evtFixed = new HashMap<>();
 
-        while (!entries.keySet().equals(data.data.keySet())) {
+        Map<Integer, InconsistentMapping> inconsistent = data.data.entrySet().stream()
+            .filter(entry -> !entry.getValue().consistent)
+            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        while (!evtEntries.keySet().equals(inconsistent.keySet())) {
             if (!evtDeq.isEmpty()) {
                 CacheConsistencyViolationEvent evt = evtDeq.remove();
 
-                entries.putAll(evt.getEntries()); // Optimistic and read committed transactions produce per key fixes.
+                assertEquals(atomicityMode() == TRANSACTIONAL ? data.strategy : ReadRepairStrategy.CHECK_ONLY, evt.getStrategy());
+
+                // Optimistic and read committed transactions produce per key fixes.
+                evtEntries.putAll(evt.getEntries());
+                evtFixed.putAll(evt.getFixedEntries());
             }
         }
 
-        int fixedCnt = 0;
-
-        for (Map.Entry<Integer, InconsistentMapping> mapping : data.data.entrySet()) {
+        for (Map.Entry<Integer, InconsistentMapping> mapping : inconsistent.entrySet()) {
             Integer key = mapping.getKey();
-            Integer latest = mapping.getValue().latest;
+            Integer fixed = mapping.getValue().fixed;
+            Integer primary = mapping.getValue().primary;
+            boolean repairable = mapping.getValue().repairable;
 
-            Object fixedVal = null;
+            if (!repairable)
+                assertNotNull(e);
 
-            Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> values = entries.get(key);
+            if (e == null) {
+                assertTrue(repairable);
+                assertTrue(evtFixed.containsKey(key));
+                assertEquals(fixed, evtFixed.get(key));
+            }
+            // Repairable but not repaired (because of irreparable entry at the same tx) entries.
+            else if (e.irreparableKeys().contains(key) || (e.repairableKeys() != null && e.repairableKeys().contains(key)))
+                assertFalse(evtFixed.containsKey(key));
 
-            if (values != null)
-                for (Map.Entry<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> infoEntry : values.entrySet()) {
-                    ClusterNode node = infoEntry.getKey();
-                    CacheConsistencyViolationEvent.EntryInfo info = infoEntry.getValue();
+            Map<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> evtEntryInfos = evtEntries.get(key);
 
-                    if (info.isCorrect()) {
-                        assertNull(fixedVal);
+            if (evtEntryInfos != null)
+                for (Map.Entry<ClusterNode, CacheConsistencyViolationEvent.EntryInfo> evtEntryInfo : evtEntryInfos.entrySet()) {
+                    ClusterNode node = evtEntryInfo.getKey();
+                    CacheConsistencyViolationEvent.EntryInfo info = evtEntryInfo.getValue();
 
-                        fixedVal = info.getValue();
+                    if (info.isCorrect())
+                        assertEquals(fixed, info.getValue());
 
-                        fixedCnt++;
-                    }
-
-                    if (info.isPrimary())
+                    if (info.isPrimary()) {
+                        assertEquals(primary, info.getValue());
                         assertEquals(node, primaryNode(key, DEFAULT_CACHE_NAME).cluster().localNode());
+                    }
                 }
-
-            if (latest != null)
-                assertEquals(checkFixed ? latest : null, fixedVal);
         }
 
-        assertEquals(checkFixed ? data.data.size() : 0, fixedCnt);
+        int expectedFixedCnt = inconsistent.size() -
+            (e != null ? (e.repairableKeys() != null ? e.repairableKeys().size() : 0) + e.irreparableKeys().size() : 0);
+
+        assertEquals(expectedFixedCnt, evtFixed.size());
 
         assertTrue(evtDeq.isEmpty());
     }
@@ -222,42 +266,87 @@
         throws Exception {
         IgniteCache<Integer, Integer> cache = initiator.getOrCreateCache(DEFAULT_CACHE_NAME);
 
-        for (int i = 0; i < ThreadLocalRandom.current().nextInt(1, 10); i++) {
+        ThreadLocalRandom rnd = ThreadLocalRandom.current();
+
+        for (int i = 0; i < rnd.nextInt(1, 10); i++) {
+            ReadRepairStrategy[] strategies = ReadRepairStrategy.values();
+
+            ReadRepairStrategy strategy = strategies[rnd.nextInt(strategies.length)];
+
             Map<Integer, InconsistentMapping> results = new TreeMap<>(); // Sorted to avoid warning.
 
-            for (int j = 0; j < cnt; j++) {
-                InconsistentMapping res = setDifferentValuesForSameKey(++iterableKey, misses, nulls);
+            try {
+                for (int j = 0; j < cnt; j++) {
+                    InconsistentMapping res = setDifferentValuesForSameKey(++iterableKey, misses, nulls, strategy);
 
-                results.put(iterableKey, res);
-            }
-
-            for (Ignite node : G.allGrids()) { // Check that cache filled properly.
-                Map<Integer, Integer> all =
-                    node.<Integer, Integer>getOrCreateCache(DEFAULT_CACHE_NAME).getAll(results.keySet());
-
-                for (Map.Entry<Integer, Integer> entry : all.entrySet()) {
-                    Integer key = entry.getKey();
-                    Integer val = entry.getValue();
-
-                    Integer exp = results.get(key).mapping.get(node); // Should read from itself (backup or primary).
-
-                    if (exp == null)
-                        exp = results.get(key).primary; // Should read from primary (not a partition owner).
-
-                    assertEquals(exp, val);
+                    results.put(iterableKey, res);
                 }
-            }
 
-            c.accept(new ReadRepairData(cache, results, raw, async));
+                for (Ignite node : G.allGrids()) { // Check that cache filled properly.
+                    Map<Integer, Integer> all =
+                        node.<Integer, Integer>getOrCreateCache(DEFAULT_CACHE_NAME).getAll(results.keySet());
+
+                    for (Map.Entry<Integer, Integer> entry : all.entrySet()) {
+                        Integer key = entry.getKey();
+                        Integer val = entry.getValue();
+
+                        T2<Integer, GridCacheVersion> valVer = results.get(key).mapping.get(node);
+
+                        Integer exp;
+
+                        if (valVer != null)
+                            exp = valVer.get1(); // Should read from itself (backup or primary).
+                        else
+                            exp = results.get(key).primary; // Or read from primary (when not a partition owner).
+
+                        assertEquals(exp, val);
+                    }
+                }
+
+                c.accept(new ReadRepairData(cache, results, raw, async, strategy));
+            }
+            catch (Throwable th) {
+                StringBuilder sb = new StringBuilder();
+
+                sb.append("Read Repair test failed [")
+                    .append("cache=").append(cache.getName())
+                    .append(", strategy=").append(strategy)
+                    .append("]\n");
+
+                for (Map.Entry<Integer, InconsistentMapping> entry : results.entrySet()) {
+                    sb.append("Key: ").append(entry.getKey()).append("\n");
+
+                    InconsistentMapping mapping = entry.getValue();
+
+                    sb.append(" Random data [primary=").append(mapping.primary)
+                        .append(", fixed=").append(mapping.fixed)
+                        .append(", repairable=").append(mapping.repairable)
+                        .append(", consistent=").append(mapping.consistent)
+                        .append("]\n");
+
+                    sb.append("  Distribution: \n");
+
+                    for (Map.Entry<Ignite, T2<Integer, GridCacheVersion>> dist : mapping.mapping.entrySet()) {
+                        sb.append("   Node: ").append(dist.getKey().name()).append("\n");
+                        sb.append("    Value: ").append(dist.getValue().get1()).append("\n");
+                        sb.append("    Version: ").append(dist.getValue().get2()).append("\n");
+                    }
+
+                    sb.append("\n");
+                }
+
+                throw new Exception(sb.toString(), th);
+            }
         }
     }
 
     /**
      *
      */
-    private InconsistentMapping setDifferentValuesForSameKey(int key, boolean misses, boolean nulls) throws Exception {
+    private InconsistentMapping setDifferentValuesForSameKey(int key, boolean misses, boolean nulls,
+        ReadRepairStrategy strategy) throws Exception {
         List<Ignite> nodes = new ArrayList<>();
-        Map<Ignite, Integer> mapping = new HashMap<>();
+        Map<Ignite, T2<Integer, GridCacheVersion>> mapping = new HashMap<>();
 
         Ignite primary = primaryNode(key, DEFAULT_CACHE_NAME);
 
@@ -275,116 +364,223 @@
         if (rnd.nextBoolean()) // Random order.
             Collections.shuffle(nodes);
 
-        GridCacheVersionManager mgr =
-            ((GridCacheAdapter)(grid(1)).cachex(DEFAULT_CACHE_NAME).cache()).context().shared().versions();
+        IgniteInternalCache<Integer, Integer> internalCache = (grid(1)).cachex(DEFAULT_CACHE_NAME);
+
+        GridCacheVersionManager mgr = ((GridCacheAdapter)internalCache.cache()).context().shared().versions();
 
         int incVal = 0;
         Integer primVal = null;
+        Collection<T2<Integer, GridCacheVersion>> vals = new ArrayList<>();
 
-        if (misses)
-            nodes = nodes.subList(0, rnd.nextInt(1, nodes.size()));
+        if (misses) {
+            List<Ignite> keeped = nodes.subList(0, rnd.nextInt(1, nodes.size()));
 
-        int rmvLimit = rnd.nextInt(nodes.size());
+            nodes.stream()
+                .filter(node -> !keeped.contains(node))
+                .forEach(node -> {
+                    T2<Integer, GridCacheVersion> nullT2 = new T2<>(null, null);
+
+                    vals.add(nullT2);
+                    mapping.put(node, nullT2);
+                });  // Recording nulls (missed values).
+
+            nodes = keeped;
+        }
+
+        boolean rmvd = false;
 
         boolean incVer = rnd.nextBoolean();
 
         GridCacheVersion ver = null;
 
         for (Ignite node : nodes) {
-            IgniteInternalCache cache = ((IgniteEx)node).cachex(DEFAULT_CACHE_NAME);
+            IgniteInternalCache<Integer, Integer> cache = ((IgniteEx)node).cachex(DEFAULT_CACHE_NAME);
 
-            GridCacheAdapter adapter = ((GridCacheAdapter)cache.cache());
+            GridCacheAdapter<Integer, Integer> adapter = (GridCacheAdapter)cache.cache();
 
             GridCacheEntryEx entry = adapter.entryEx(key);
 
             if (ver == null || incVer)
                 ver = mgr.next(entry.context().kernalContext().discovery().topologyVersion()); // Incremental version.
 
-            boolean rmv = nulls && rnd.nextBoolean() && --rmvLimit >= 0;
+            boolean rmv = nulls && (!rmvd || rnd.nextBoolean());
 
-            Integer val = rmv ? null : ++incVal;
+            Integer val = rmv ? null : rnd.nextBoolean()/*increment or same as previously*/ ? ++incVal : incVal;
 
-            boolean init = entry.initialValue(
-                new CacheObjectImpl(rmv ? -1 : val, null), // Incremental value.
-                ver,
-                0,
-                0,
-                false,
-                AffinityTopologyVersion.NONE,
-                GridDrType.DR_NONE,
-                false,
-                false);
+            T2<Integer, GridCacheVersion> valVer = new T2<>(val, val != null ? ver : null);
 
-            if (rmv) {
-                if (cache.configuration().getAtomicityMode() == ATOMIC)
-                    entry.innerUpdate(
-                        ver,
-                        ((IgniteEx)node).localNode().id(),
-                        ((IgniteEx)node).localNode().id(),
-                        GridCacheOperation.DELETE,
-                        null,
-                        null,
-                        false,
-                        false,
-                        false,
-                        false,
-                        null,
-                        false,
-                        false,
-                        false,
-                        false,
-                        AffinityTopologyVersion.NONE,
-                        null,
-                        GridDrType.DR_NONE,
-                        0,
-                        0,
-                        null,
-                        false,
-                        false,
-                        null,
-                        null,
-                        null,
-                        null,
-                        false);
+            vals.add(valVer);
+            mapping.put(node, valVer);
+
+            GridKernalContext kctx = ((IgniteEx)node).context();
+
+            byte[] bytes = kctx.cacheObjects().marshal(entry.context().cacheObjectContext(), rmv ? -1 : val); // Incremental value.
+
+            try {
+                kctx.cache().context().database().checkpointReadLock();
+
+                boolean init = entry.initialValue(
+                    new CacheObjectImpl(null, bytes),
+                    ver,
+                    0,
+                    0,
+                    false,
+                    AffinityTopologyVersion.NONE,
+                    GridDrType.DR_NONE,
+                    false,
+                    false);
+
+                if (rmv) {
+                    if (cache.configuration().getAtomicityMode() == ATOMIC)
+                        entry.innerUpdate(
+                            ver,
+                            ((IgniteEx)node).localNode().id(),
+                            ((IgniteEx)node).localNode().id(),
+                            GridCacheOperation.DELETE,
+                            null,
+                            null,
+                            false,
+                            false,
+                            false,
+                            false,
+                            null,
+                            false,
+                            false,
+                            false,
+                            false,
+                            AffinityTopologyVersion.NONE,
+                            null,
+                            GridDrType.DR_NONE,
+                            0,
+                            0,
+                            null,
+                            false,
+                            false,
+                            null,
+                            null,
+                            null,
+                            null,
+                            false);
+                    else
+                        entry.innerRemove(
+                            null,
+                            ((IgniteEx)node).localNode().id(),
+                            ((IgniteEx)node).localNode().id(),
+                            false,
+                            false,
+                            false,
+                            false,
+                            false,
+                            null,
+                            AffinityTopologyVersion.NONE,
+                            CU.empty0(),
+                            GridDrType.DR_NONE,
+                            null,
+                            null,
+                            null,
+                            1L);
+
+                    rmvd = true;
+
+                    assertFalse(entry.hasValue());
+                }
                 else
-                    entry.innerRemove(
-                        null,
-                        ((IgniteEx)node).localNode().id(),
-                        ((IgniteEx)node).localNode().id(),
-                        false,
-                        false,
-                        false,
-                        false,
-                        false,
-                        null,
-                        AffinityTopologyVersion.NONE,
-                        CU.empty0(),
-                        GridDrType.DR_NONE,
-                        null,
-                        null,
-                        null,
-                        1L);
+                    assertTrue(entry.hasValue());
 
-                assertFalse(entry.hasValue());
+                assertTrue("iterableKey " + key + " already inited", init);
+
+                if (node.equals(primary))
+                    primVal = val;
             }
-            else
-                assertTrue(entry.hasValue());
-
-            assertTrue("iterableKey " + key + " already inited", init);
-
-            mapping.put(node, val);
-
-            if (node.equals(primary))
-                primVal = val;
+            finally {
+                ((IgniteEx)node).context().cache().context().database().checkpointReadUnlock();
+            }
         }
 
-        if (!nulls)
-            assertEquals(nodes.size(), new HashSet<>(mapping.values()).size()); // Each node have unique value.
+        assertEquals(vals.size(), mapping.size());
+        assertEquals(vals.size(),
+            internalCache.configuration().getCacheMode() == REPLICATED ? serverNodesCount() : backupsCount() + 1);
 
         if (!misses && !nulls)
             assertTrue(primVal != null); // Primary value set.
 
-        return new InconsistentMapping(mapping, primVal, incVer ? incVal : null /*Any*/);
+        Integer fixed;
+
+        boolean consistent;
+        boolean repairable;
+
+        if (vals.stream().distinct().count() == 1) { // Consistent value.
+            consistent = true;
+            repairable = true;
+            fixed = vals.iterator().next().getKey();
+        }
+        else {
+            consistent = false;
+            repairable = atomicityMode() != ATOMIC; // Currently, Atomic caches can not be repaired.
+
+            switch (strategy) {
+                case LWW:
+                    if (misses || rmvd || !incVer) {
+                        repairable = false;
+
+                        fixed = Integer.MIN_VALUE; // Should never be returned.
+                    }
+                    else
+                        fixed = incVal;
+
+                    break;
+
+                case PRIMARY:
+                    fixed = primVal;
+
+                    break;
+
+                case RELATIVE_MAJORITY:
+                    fixed = Integer.MIN_VALUE; // Should never be returned.
+
+                    Map<T2<Integer, GridCacheVersion>, Integer> counts = new HashMap<>();
+
+                    for (T2<Integer, GridCacheVersion> val : vals) {
+                        counts.putIfAbsent(val, 0);
+
+                        counts.compute(val, (k, v) -> v + 1);
+                    }
+
+                    int[] sorted = counts.values().stream().sorted(Comparator.reverseOrder()).mapToInt(v -> v).toArray();
+
+                    int max = sorted[0];
+
+                    if (sorted.length > 1 && sorted[1] == max)
+                        repairable = false;
+
+                    if (repairable)
+                        for (Map.Entry<T2<Integer, GridCacheVersion>, Integer> count : counts.entrySet())
+                            if (count.getValue().equals(max)) {
+                                fixed = count.getKey().getKey();
+
+                                break;
+                            }
+
+                    break;
+
+                case REMOVE:
+                    fixed = null;
+
+                    break;
+
+                case CHECK_ONLY:
+                    repairable = false;
+
+                    fixed = Integer.MIN_VALUE; // Should never be returned.
+
+                    break;
+
+                default:
+                    throw new UnsupportedOperationException(strategy.toString());
+            }
+        }
+
+        return new InconsistentMapping(mapping, primVal, fixed, repairable, consistent);
     }
 
     /**
@@ -392,16 +588,19 @@
      */
     protected static final class ReadRepairData {
         /** Initiator's cache. */
-        IgniteCache<Integer, Integer> cache;
+        final IgniteCache<Integer, Integer> cache;
 
         /** Generated data across topology per key mapping. */
-        Map<Integer, InconsistentMapping> data;
+        final Map<Integer, InconsistentMapping> data;
 
         /** Raw read flag. True means required GetEntry() instead of get(). */
-        boolean raw;
+        final boolean raw;
 
         /** Async read flag. */
-        boolean async;
+        final boolean async;
+
+        /** Strategy. */
+        final ReadRepairStrategy strategy;
 
         /**
          *
@@ -410,11 +609,20 @@
             IgniteCache<Integer, Integer> cache,
             Map<Integer, InconsistentMapping> data,
             boolean raw,
-            boolean async) {
+            boolean async,
+            ReadRepairStrategy strategy) {
             this.cache = cache;
             this.data = data;
             this.raw = raw;
             this.async = async;
+            this.strategy = strategy;
+        }
+
+        /**
+         *
+         */
+        boolean repairable() {
+            return data.values().stream().allMatch(mapping -> mapping.repairable);
         }
     }
 
@@ -423,21 +631,30 @@
      */
     protected static final class InconsistentMapping {
         /** Value per node. */
-        Map<Ignite, Integer> mapping;
+        final Map<Ignite, T2<Integer, GridCacheVersion>> mapping;
 
         /** Primary node's value. */
-        Integer primary;
+        final Integer primary;
 
-        /** Latest value across the topology. */
-        Integer latest;
+        /** Expected fix result. */
+        final Integer fixed;
+
+        /** Inconsistency can be repaired using the specified strategy. */
+        final boolean repairable;
+
+        /** Consistent value. */
+        final boolean consistent;
 
         /**
          *
          */
-        public InconsistentMapping(Map<Ignite, Integer> mapping, Integer primary, Integer latest) {
+        public InconsistentMapping(Map<Ignite, T2<Integer, GridCacheVersion>> mapping, Integer primary, Integer fixed,
+            boolean repairable, boolean consistent) {
             this.mapping = new HashMap<>(mapping);
             this.primary = primary;
-            this.latest = latest;
+            this.fixed = fixed;
+            this.repairable = repairable;
+            this.consistent = consistent;
         }
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AtomicReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AtomicReadRepairTest.java
deleted file mode 100644
index ac65b95..0000000
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/AtomicReadRepairTest.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * 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.cache.consistency;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Consumer;
-import javax.cache.CacheException;
-import org.apache.ignite.Ignite;
-import org.apache.ignite.IgniteCache;
-import org.apache.ignite.cache.CacheAtomicityMode;
-import org.apache.ignite.cache.CacheEntry;
-import org.apache.ignite.internal.processors.cache.distributed.near.consistency.IgniteConsistencyViolationException;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-/**
- * Ideally, this test should be equal to {@link ImplicitTransactionalReadRepairTest} except atomicy mode
- * configuration.
- *
- * Implement repair on atomic caches to make this happen :)
- */
-@RunWith(Parameterized.class)
-public class AtomicReadRepairTest extends ImplicitTransactionalReadRepairTest {
-    /**
-     *
-     */
-    private static final Consumer<ReadRepairData> GET_CHECK_AND_FAIL = (data) -> {
-        IgniteCache<Integer, Integer> cache = data.cache;
-        Set<Integer> keys = data.data.keySet();
-        boolean raw = data.raw;
-        boolean async = data.async;
-
-        assert keys.size() == 1;
-
-        for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) { // Once.
-            try {
-                Integer key = entry.getKey();
-
-                Integer res =
-                    raw ?
-                        async ?
-                            cache.withReadRepair().getEntryAsync(key).get().getValue() :
-                            cache.withReadRepair().getEntry(key).getValue() :
-                        async ?
-                            cache.withReadRepair().getAsync(key).get() :
-                            cache.withReadRepair().get(key);
-
-                fail("Should not happen.");
-            }
-            catch (CacheException e) {
-                assertTrue(e.getCause() instanceof IgniteConsistencyViolationException);
-            }
-        }
-    };
-
-    /**
-     *
-     */
-    private static final Consumer<ReadRepairData> GETALL_CHECK_AND_FAIL = (data) -> {
-        IgniteCache<Integer, Integer> cache = data.cache;
-        Set<Integer> keys = data.data.keySet();
-        boolean raw = data.raw;
-        boolean async = data.async;
-
-        assert !keys.isEmpty();
-
-        try {
-            if (raw) {
-                Collection<CacheEntry<Integer, Integer>> res =
-                    async ?
-                        cache.withReadRepair().getEntriesAsync(keys).get() :
-                        cache.withReadRepair().getEntries(keys);
-
-                for (CacheEntry<Integer, Integer> entry : res) {
-                    Integer latest = data.data.get(entry.getKey()).latest;
-
-                    if (latest != null)
-                        assertEquals(latest, entry.getValue());
-                }
-            }
-            else {
-                Map<Integer, Integer> res =
-                    async ?
-                        cache.withReadRepair().getAllAsync(keys).get() :
-                        cache.withReadRepair().getAll(keys);
-
-                for (Map.Entry<Integer, Integer> entry : res.entrySet()) {
-                    Integer latest = data.data.get(entry.getKey()).latest;
-
-                    if (latest != null)
-                        assertEquals(latest, entry.getValue());
-                }
-            }
-
-            fail("Should not happen.");
-        }
-        catch (CacheException e) {
-            assertTrue(e.getCause() instanceof IgniteConsistencyViolationException);
-        }
-    };
-
-    /**
-     *
-     */
-    private static final Consumer<ReadRepairData> CONTAINS_CHECK_AND_FAIL = (data) -> {
-        IgniteCache<Integer, Integer> cache = data.cache;
-        Set<Integer> keys = data.data.keySet();
-        boolean async = data.async;
-
-        assert keys.size() == 1;
-
-        for (Map.Entry<Integer, InconsistentMapping> entry : data.data.entrySet()) { // Once.
-            try {
-                Integer key = entry.getKey();
-
-                boolean res = async ?
-                    cache.withReadRepair().containsKeyAsync(key).get() :
-                    cache.withReadRepair().containsKey(key);
-
-                fail("Should not happen.");
-            }
-            catch (Exception e) {
-                assertTrue(e.getCause() instanceof IgniteConsistencyViolationException);
-            }
-        }
-    };
-
-    /**
-     *
-     */
-    private static final Consumer<ReadRepairData> CONTAINS_ALL_CHECK_AND_FAIL = (data) -> {
-        IgniteCache<Integer, Integer> cache = data.cache;
-        Set<Integer> keys = data.data.keySet();
-        boolean async = data.async;
-
-        try {
-            boolean res = async ?
-                cache.withReadRepair().containsKeysAsync(keys).get() :
-                cache.withReadRepair().containsKeys(keys);
-
-            fail("Should not happen.");
-        }
-        catch (Exception e) {
-            assertTrue(e.getCause() instanceof IgniteConsistencyViolationException);
-        }
-    };
-
-    /** {@inheritDoc} */
-    @Override protected CacheAtomicityMode atomicyMode() {
-        return CacheAtomicityMode.ATOMIC;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void testGet(Ignite initiator, Integer cnt, boolean all) throws Exception {
-        prepareAndCheck(
-            initiator,
-            cnt,
-            raw,
-            async,
-            misses,
-            nulls,
-            (ReadRepairData data) -> {
-                if (all)
-                    GETALL_CHECK_AND_FAIL.accept(data);
-                else
-                    GET_CHECK_AND_FAIL.accept(data);
-
-                check(data, true, false);
-            });
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void testContains(Ignite initiator, Integer cnt, boolean all) throws Exception {
-        prepareAndCheck(
-            initiator,
-            cnt,
-            raw,
-            async,
-            misses,
-            nulls,
-            (ReadRepairData data) -> {
-                if (all)
-                    CONTAINS_ALL_CHECK_AND_FAIL.accept(data);
-                else
-                    CONTAINS_CHECK_AND_FAIL.accept(data);
-
-                check(data, true, false);
-            });
-    }
-}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/AtomicReadRepairTest.java
similarity index 67%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/AtomicReadRepairTest.java
index a244658..e4cc2b8 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/AtomicReadRepairTest.java
@@ -15,16 +15,19 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
-import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 /**
  *
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+@RunWith(Parameterized.class)
+public class AtomicReadRepairTest extends ImplicitTransactionalReadRepairTest {
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override protected CacheAtomicityMode atomicityMode() {
+        return CacheAtomicityMode.ATOMIC;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ExplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ExplicitTransactionalReadRepairTest.java
similarity index 61%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ExplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ExplicitTransactionalReadRepairTest.java
index 2935bc9..0314f53 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ExplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ExplicitTransactionalReadRepairTest.java
@@ -15,12 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.function.Consumer;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.processors.cache.consistency.AbstractFullSetReadRepairTest;
 import org.apache.ignite.transactions.Transaction;
 import org.apache.ignite.transactions.TransactionConcurrency;
 import org.apache.ignite.transactions.TransactionIsolation;
@@ -85,58 +87,12 @@
             async,
             misses,
             nulls,
-            (ReadRepairData data) -> {
+            (ReadRepairData data) -> repairIfRepairable.accept(data, () -> {
                 boolean fixByOtherTx = concurrency == TransactionConcurrency.OPTIMISTIC ||
                     isolation == TransactionIsolation.READ_COMMITTED;
 
-                try (Transaction tx = initiator.transactions().txStart(concurrency, isolation)) {
-                    // Recovery (inside tx).
-                    if (all)
-                        GETALL_CHECK_AND_FIX.accept(data);
-                    else
-                        GET_CHECK_AND_FIX.accept(data);
-
-                    check(data, fixByOtherTx, true); // Checks (inside tx).
-
-                    try {
-                        tx.commit();
-                    }
-                    catch (TransactionRollbackException e) {
-                        fail("Should not happen. " + e);
-                    }
-                }
-
-                check(data, !fixByOtherTx, true); // Checks (outside tx).
-            });
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void testGetNull(Ignite initiator) throws Exception {
-        prepareAndCheck(
-            initiator,
-            1,
-            raw,
-            async,
-            misses,
-            nulls,
-            (ReadRepairData data) -> {
-                try (Transaction tx = initiator.transactions().txStart(concurrency, isolation)) {
-                    GET_NULL.accept(data);
-
-                    checkEventMissed();
-
-                    try {
-                        tx.commit();
-                    }
-                    catch (TransactionRollbackException e) {
-                        fail("Should not happen. " + e);
-                    }
-                }
-
-                GET_NULL.accept(data); // Checks (outside tx).
-
-                checkEventMissed();
-            });
+                testReadRepair(initiator, data, all ? GETALL_CHECK_AND_FIX : GET_CHECK_AND_FIX, fixByOtherTx, true);
+            }));
     }
 
     /** {@inheritDoc} */
@@ -148,28 +104,49 @@
             async,
             misses,
             nulls,
-            (ReadRepairData data) -> {
+            (ReadRepairData data) -> repairIfRepairable.accept(data, () -> {
                 // "Contains" works like optimistic() || readCommitted() and always fixed by other tx.
-                boolean fixByOtherTx = true;
+                testReadRepair(initiator, data, all ? CONTAINS_ALL_CHECK_AND_FIX : CONTAINS_CHECK_AND_FIX, true, true);
+            }));
+    }
 
-                try (Transaction tx = initiator.transactions().txStart(concurrency, isolation)) {
-                    // Recovery (inside tx).
-                    if (all)
-                        CONTAINS_ALL_CHECK_AND_FIX.accept(data);
-                    else
-                        CONTAINS_CHECK_AND_FIX.accept(data);
+    /** {@inheritDoc} */
+    @Override protected void testGetNull(Ignite initiator, Integer cnt, boolean all) throws Exception {
+        prepareAndCheck(
+            initiator,
+            cnt,
+            raw,
+            async,
+            misses,
+            nulls,
+            (ReadRepairData data) -> testReadRepair(initiator, data, all ? GET_ALL_NULL : GET_NULL, false, false));
+    }
 
-                    check(data, fixByOtherTx, true); // Checks (inside tx).
+    /**
+     *
+     */
+    private void testReadRepair(Ignite initiator, ReadRepairData data, Consumer<ReadRepairData> readOp,
+        boolean fixByOtherTx, boolean hit) {
+        try (Transaction tx = initiator.transactions().txStart(concurrency, isolation)) {
+            // Recovery (inside tx).
+            readOp.accept(data);
 
-                    try {
-                        tx.commit();
-                    }
-                    catch (TransactionRollbackException e) {
-                        fail("Should not happen. " + e);
-                    }
-                }
+            if (hit) // Checks (inside tx).
+                check(data, null, fixByOtherTx); // Hit.
+            else
+                checkEventMissed(); // Miss.
 
-                check(data, !fixByOtherTx, true); // Checks (outside tx).
-            });
+            try {
+                tx.commit();
+            }
+            catch (TransactionRollbackException e) {
+                fail("Should not happen. " + e);
+            }
+        }
+
+        if (hit) // Checks (outside tx).
+            check(data, null, !fixByOtherTx); // Hit.
+        else
+            checkEventMissed(); // Miss.
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ImplicitTransactionalReadRepairTest.java
similarity index 72%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ImplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ImplicitTransactionalReadRepairTest.java
index a6019fd..dae70a2 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ImplicitTransactionalReadRepairTest.java
@@ -15,12 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.function.Consumer;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.processors.cache.consistency.AbstractFullSetReadRepairTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
@@ -69,34 +71,8 @@
             async,
             misses,
             nulls,
-            (ReadRepairData data) -> {
-                if (all)
-                    GETALL_CHECK_AND_FIX.accept(data);
-                else
-                    GET_CHECK_AND_FIX.accept(data);
-
-                check(data, true, true);
-            });
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void testGetNull(Ignite initiator) throws Exception {
-        prepareAndCheck(
-            initiator,
-            1,
-            raw,
-            async,
-            misses,
-            nulls,
-            (ReadRepairData data) -> {
-                GET_NULL.accept(data); // first attempt.
-
-                checkEventMissed();
-
-                GET_NULL.accept(data); // second attempt (checks first attempt causes no changes/fixes/etc).
-
-                checkEventMissed();
-            });
+            (ReadRepairData data) -> repairIfRepairable.accept(data,
+                () -> testReadRepair(data, all ? GETALL_CHECK_AND_FIX : GET_CHECK_AND_FIX, true)));
     }
 
     /** {@inheritDoc} */
@@ -108,13 +84,31 @@
             async,
             misses,
             nulls,
-            (ReadRepairData data) -> {
-                if (all)
-                    CONTAINS_ALL_CHECK_AND_FIX.accept(data);
-                else
-                    CONTAINS_CHECK_AND_FIX.accept(data);
+            (ReadRepairData data) -> repairIfRepairable.accept(data,
+                () -> testReadRepair(data, all ? CONTAINS_ALL_CHECK_AND_FIX : CONTAINS_CHECK_AND_FIX, true)));
+    }
 
-                check(data, true, true);
-            });
+    /** {@inheritDoc} */
+    @Override protected void testGetNull(Ignite initiator, Integer cnt, boolean all) throws Exception {
+        prepareAndCheck(
+            initiator,
+            cnt,
+            raw,
+            async,
+            misses,
+            nulls,
+            (ReadRepairData data) -> testReadRepair(data, all ? GET_ALL_NULL : GET_NULL, false));
+    }
+
+    /**
+     *
+     */
+    private void testReadRepair(ReadRepairData data, Consumer<ReadRepairData> readOp, boolean hit) {
+        readOp.accept(data);
+
+        if (hit)
+            check(data, null, true); // Hit.
+        else
+            checkEventMissed(); // Miss.
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedExplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedExplicitTransactionalReadRepairTest.java
similarity index 93%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedExplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedExplicitTransactionalReadRepairTest.java
index c5dcd28..aad3f15 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedExplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedExplicitTransactionalReadRepairTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 import org.apache.ignite.cache.CacheMode;
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedImplicitTransactionalReadRepairTest.java
similarity index 93%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedImplicitTransactionalReadRepairTest.java
index a244658..8f84c57 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/ReplicatedImplicitTransactionalReadRepairTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 import org.apache.ignite.cache.CacheMode;
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupExplicitTransactionalReadRepairTest.java
similarity index 93%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupExplicitTransactionalReadRepairTest.java
index 13ed4bd..6a0f494 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupExplicitTransactionalReadRepairTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 /**
  *
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupImplicitTransactionalReadRepairTest.java
similarity index 93%
rename from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupImplicitTransactionalReadRepairTest.java
rename to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupImplicitTransactionalReadRepairTest.java
index 9d77fd4..07c5cd0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/inmem/SingleBackupImplicitTransactionalReadRepairTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.inmem;
 
 /**
  *
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsAtomicReadRepairTest.java
similarity index 72%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsAtomicReadRepairTest.java
index a244658..86a02b5 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsAtomicReadRepairTest.java
@@ -15,16 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.persistence;
 
-import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.AtomicReadRepairTest;
 
 /**
  *
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+public class PdsAtomicReadRepairTest extends AtomicReadRepairTest {
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override protected boolean persistenceEnabled() {
+        return true;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsExplicitTransactionalReadRepairTest.java
similarity index 70%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsExplicitTransactionalReadRepairTest.java
index a244658..eb74690 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsExplicitTransactionalReadRepairTest.java
@@ -15,16 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.persistence;
 
-import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ExplicitTransactionalReadRepairTest;
 
 /**
  *
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+public class PdsExplicitTransactionalReadRepairTest extends ExplicitTransactionalReadRepairTest {
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override protected boolean persistenceEnabled() {
+        return true;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsImplicitTransactionalReadRepairTest.java
similarity index 70%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
copy to modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsImplicitTransactionalReadRepairTest.java
index a244658..f73cf79 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/ReplicatedImplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/persistence/PdsImplicitTransactionalReadRepairTest.java
@@ -15,16 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.cache.consistency.persistence;
 
-import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ImplicitTransactionalReadRepairTest;
 
 /**
  *
  */
-public class ReplicatedImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
+public class PdsImplicitTransactionalReadRepairTest extends ImplicitTransactionalReadRepairTest {
     /** {@inheritDoc} */
-    @Override protected CacheMode cacheMode() {
-        return CacheMode.REPLICATED;
+    @Override protected boolean persistenceEnabled() {
+        return true;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/datastructures/OutOfMemoryVolatileRegionTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/datastructures/OutOfMemoryVolatileRegionTest.java
index 58e6290..bce9c87 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/datastructures/OutOfMemoryVolatileRegionTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/datastructures/OutOfMemoryVolatileRegionTest.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.SystemDataRegionConfiguration;
 import org.apache.ignite.failure.AbstractFailureHandler;
 import org.apache.ignite.failure.FailureContext;
 import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
@@ -55,8 +56,11 @@
 
         cfg.setDataStorageConfiguration(new DataStorageConfiguration()
             .setPageSize(4096)
-            .setSystemRegionInitialSize(DATA_REGION_SIZE)
-            .setSystemRegionMaxSize(DATA_REGION_SIZE)
+            .setSystemDataRegionConfiguration(
+                    new SystemDataRegionConfiguration()
+                            .setInitialSize(DATA_REGION_SIZE)
+                            .setMaxSize(DATA_REGION_SIZE)
+            )
             .setDefaultDataRegionConfiguration(
                 new DataRegionConfiguration()
                     .setPersistenceEnabled(true)
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/distributed/dht/WaitForBackupsOnShutdownSystemPropertyTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/distributed/dht/WaitForBackupsOnShutdownSystemPropertyTest.java
new file mode 100644
index 0000000..8ae91b8
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/distributed/dht/WaitForBackupsOnShutdownSystemPropertyTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.cache.distributed.dht;
+
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.testframework.ListeningTestLogger;
+import org.apache.ignite.testframework.LogListener;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN;
+
+/**
+ * Check a log message if {@link IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN} is used.
+ */
+public class WaitForBackupsOnShutdownSystemPropertyTest extends GridCommonAbstractTest {
+    /** Listening test logger. */
+    private ListeningTestLogger listeningLog;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        listeningLog = new ListeningTestLogger(log);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setGridLogger(listeningLog);
+    }
+
+    /**
+     * Check the message is printed if IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN is used.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN, value = "false")
+    public void testWaitForBackupsOnShutdownPropertyExists() throws Exception {
+        LogListener lnsr = LogListener.matches("IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN system property " +
+                "is deprecated and will be removed in a future version. Use ShutdownPolicy instead.")
+            .build();
+
+        listeningLog.registerListener(lnsr);
+
+        startGrid();
+
+        assertTrue("The message was not found", lnsr.check());
+    }
+
+    /**
+     * Check the message is not printed if IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN is not used.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testWaitForBackupsOnShutdownPropertyNotExists() throws Exception {
+        LogListener lnsr = LogListener.matches("IGNITE_WAIT_FOR_BACKUPS_ON_SHUTDOWN system property " +
+                "is deprecated and will be removed in a future version. Use ShutdownPolicy instead.")
+            .build();
+
+        listeningLog.registerListener(lnsr);
+
+        startGrid();
+
+        assertFalse("The message was found", lnsr.check());
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCheckpointMapSnapshotTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCheckpointMapSnapshotTest.java
new file mode 100644
index 0000000..9d431e1
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCheckpointMapSnapshotTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.cache.persistence;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.pagemem.wal.IgniteWriteAheadLogManager;
+import org.apache.ignite.internal.pagemem.wal.WALIterator;
+import org.apache.ignite.internal.pagemem.wal.record.WALRecord;
+import org.apache.ignite.internal.processors.cache.persistence.checkpoint.CheckpointEntry.GroupStateLazyStore;
+import org.apache.ignite.internal.processors.cache.persistence.checkpoint.CheckpointMarkersStorage;
+import org.apache.ignite.internal.processors.cache.persistence.wal.FileWriteAheadLogManager;
+import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.lang.IgniteBiPredicate;
+import org.apache.ignite.plugin.AbstractTestPluginProvider;
+import org.apache.ignite.plugin.PluginContext;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_PREFER_WAL_REBALANCE;
+
+/**
+ * Tests checkpoint map snapshot.
+ */
+@WithSystemProperty(key = IGNITE_PREFER_WAL_REBALANCE, value = "true")
+@WithSystemProperty(key = IGNITE_CHECKPOINT_MAP_SNAPSHOT_THRESHOLD, value = "1")
+public class IgnitePdsCheckpointMapSnapshotTest extends GridCommonAbstractTest {
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String name) throws Exception {
+        IgniteConfiguration configuration = super.getConfiguration(name);
+
+        configuration.setDataStorageConfiguration(
+            new DataStorageConfiguration().setDefaultDataRegionConfiguration(
+                new DataRegionConfiguration().setPersistenceEnabled(true)
+            ).setCheckpointFrequency(TimeUnit.HOURS.toMillis(1))
+        );
+
+        // Plugin that creates a WAL manager that counts replays
+        configuration.setPluginProviders(new AbstractTestPluginProvider() {
+            /** {@inheritDoc} */
+            @Override public String name() {
+                return "testPlugin";
+            }
+
+            /** {@inheritDoc} */
+            @Override public <T> @Nullable T createComponent(PluginContext ctx, Class<T> cls) {
+                if (IgniteWriteAheadLogManager.class.equals(cls))
+                    return (T)new TestFileWriteAheadLogManager(((IgniteEx)ctx.grid()).context());
+
+                return null;
+            }
+        });
+
+        return configuration;
+    }
+
+    /** WAL manager that counts how many times replay has been called. */
+    private static class TestFileWriteAheadLogManager extends FileWriteAheadLogManager {
+        /** Count of times that {@link #replay(WALPointer, IgniteBiPredicate)} has been called. */
+        private final AtomicInteger replayCount = new AtomicInteger();
+
+        /** Constructor. */
+        public TestFileWriteAheadLogManager(GridKernalContext ctx) {
+            super(ctx);
+        }
+
+        /** {@link GroupStateLazyStore} class name. */
+        private static final String clsName = GroupStateLazyStore.class.getName();
+
+        /** {@inheritDoc} */
+        @Override public WALIterator replay(
+            WALPointer start,
+            @Nullable IgniteBiPredicate<WALRecord.RecordType, WALPointer> recordDeserializeFilter
+        ) throws IgniteCheckedException, StorageException {
+            Exception exception = new Exception();
+            StackTraceElement[] trace = exception.getStackTrace();
+
+            // Here we only want to record replays from GroupStateLazyStore
+            // because they're performed if the snapshot doesn't include the data for a checkpoint
+            if (Arrays.stream(trace).anyMatch(el -> el.getClassName().equals(clsName)))
+                replayCount.incrementAndGet();
+
+            return super.replay(start, recordDeserializeFilter);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        stopAllGrids();
+
+        cleanPersistenceDir();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        stopAllGrids();
+
+        cleanPersistenceDir();
+    }
+
+    /**
+     * Tests that node can restart successfully with a checkpoint map snapshot.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRestartWithCheckpointMapSnapshot() throws Exception {
+        testRestart(false);
+    }
+
+    /**
+     * Tests that node can restart successfully without a checkpoint map snapshot.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRestartWithoutCheckpointMapSnapshot() throws Exception {
+        testRestart(true);
+    }
+
+    /**
+     * Tests node restart after a series of checkpoints. Node should use a checkpoint map snapshot if it is present.
+     *
+     * @param removeSnapshot Whether to remove a snapshot of a checkpoint map.
+     * @throws Exception If failed.
+     */
+    private void testRestart(boolean removeSnapshot) throws Exception {
+        IgniteEx grid = startGrid(0);
+
+        grid.cluster().state(ClusterState.ACTIVE);
+
+        // Count of inserts and checkpoints
+        int cnt = 100;
+
+        CacheConfiguration<Integer, Integer> configuration = new CacheConfiguration<>(DEFAULT_CACHE_NAME);
+
+        IgniteCache<Integer, Integer> cache = grid.getOrCreateCache(configuration);
+
+        for (int i = 0; i < cnt; i++) {
+            cache.put(i, i);
+
+            forceCheckpoint(grid);
+        }
+
+        stopGrid(0, true);
+
+        if (removeSnapshot) {
+            // Remove checkpoint map snapshot
+            File cpDir = dbMgr(grid).checkpointManager.checkpointDirectory();
+
+            File cpSnapshotMap = new File(cpDir, CheckpointMarkersStorage.EARLIEST_CP_SNAPSHOT_FILE);
+
+            IgniteUtils.delete(cpSnapshotMap);
+
+            assertFalse(cpSnapshotMap.exists());
+        }
+
+        grid = startGrid(0);
+
+        grid.cluster().state(ClusterState.ACTIVE);
+
+        // Start new grids and wait for rebalance
+        startGrid(1);
+        startGrid(2);
+
+        awaitPartitionMapExchange();
+
+        TestFileWriteAheadLogManager wal = (TestFileWriteAheadLogManager)walMgr(grid);
+
+        cache = grid.getOrCreateCache(configuration);
+
+        // Check data in a cache
+        for (int i = 0; i < cnt; i++)
+            assertEquals(i, (int)cache.get(i));
+
+        // Get count of WAL replays that are invoked from CheckpointEntry
+        int replayCount = wal.replayCount.get();
+
+        stopGrid(1, true);
+        stopGrid(2, true);
+
+        // 1 is the count of checkpoint on start of the node (see checkpoint with reason "node started")
+        if (removeSnapshot)
+            assertEquals(cnt + 1, replayCount);
+        else
+            assertEquals(0, replayCount);
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsDestroyCacheTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsDestroyCacheTest.java
index d5f7e33..db8266d 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsDestroyCacheTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsDestroyCacheTest.java
@@ -18,14 +18,31 @@
 package org.apache.ignite.internal.processors.cache.persistence;
 
 import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import javax.cache.CacheException;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.failure.StopNodeFailureHandler;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.TestRecordingCommunicationSpi;
 import org.apache.ignite.internal.processors.cache.CacheGroupContext;
 import org.apache.ignite.internal.processors.cache.IgniteCacheOffheapManager;
+import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxPrepareRequest;
+import org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsFullMessage;
+import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteFuture;
+import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -36,6 +53,13 @@
  * Test correct clean up cache configuration data after destroying cache.
  */
 public class IgnitePdsDestroyCacheTest extends IgnitePdsDestroyCacheAbstractTest {
+    /** */
+    @Override public IgniteConfiguration getConfiguration(String instanceName) throws Exception {
+        return super.getConfiguration(instanceName)
+            .setFailureHandler(new StopNodeFailureHandler())
+            .setCommunicationSpi(new TestRecordingCommunicationSpi());
+    }
+    
     /**
      *  Test destroy non grouped caches.
      *
@@ -191,4 +215,64 @@
 
         fut.get();
     }
+
+    /**
+     * Tests correctness of concurrent cache destroy and implicit tx`s.
+     */
+    @Test
+    public void cacheDestroyWithConcImplicitTx() throws Exception {
+        final IgniteEx crd = (IgniteEx)startGridsMultiThreaded(3);
+
+        crd.cluster().state(ClusterState.ACTIVE);
+
+        crd.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME)
+            .setBackups(1).setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL).setGroupName("test"));
+
+        // Cache group with multiple caches are important here, in this case cache removals are not so rapid.
+        crd.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME + "_1")
+            .setBackups(1).setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL).setGroupName("test"));
+
+        Set<Integer> pkeys = new TreeSet<>();
+        try (final IgniteDataStreamer<Object, Object> streamer = crd.dataStreamer(DEFAULT_CACHE_NAME)) {
+            for (int i = 0; i < 100; i++) {
+                streamer.addData(i, i);
+
+                if (crd.affinity(DEFAULT_CACHE_NAME).isPrimary(crd.localNode(), i))
+                    pkeys.add(i);
+            }
+        }
+
+        TestRecordingCommunicationSpi spi = TestRecordingCommunicationSpi.spi(crd);
+
+        spi.blockMessages(GridDhtTxPrepareRequest.class, getTestIgniteInstanceName(1));
+
+        List<IgniteFuture<Boolean>> asyncRmFut = new ArrayList<>(100);
+
+        for (Integer pkey : pkeys)
+            asyncRmFut.add(crd.cache(DEFAULT_CACHE_NAME).removeAsync(pkey));
+
+        spi.blockMessages(GridDhtPartitionsFullMessage.class, getTestIgniteInstanceName(1));
+
+        IgniteInternalFuture destr = GridTestUtils.runAsync(() -> grid(1).destroyCache(DEFAULT_CACHE_NAME));
+
+        spi.waitForBlocked();
+
+        spi.stopBlock(true, (msg) -> msg.ioMessage().message() instanceof GridDhtPartitionsFullMessage);
+
+        spi.stopBlock();
+
+        destr.get();
+
+        // A little bit untipattern approach here, just because of async remapping, check
+        // GridNearOptimisticTxPrepareFutureAdapter.prepareOnTopology.
+        // With redefined Failure handler we still need the same approach: wait some time and checks that it not raises.
+        assertFalse(GridTestUtils.waitForCondition(() -> G.allGrids().size() < 3, 5_000));
+
+        try {
+            asyncRmFut.forEach(f -> f.get(getTestTimeout() / 2));
+        }
+        catch (CacheException ignore) {
+            // No op.
+        }
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsRecoveryAfterFileCorruptionTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsRecoveryAfterFileCorruptionTest.java
index aebf7f3..ceadb50 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsRecoveryAfterFileCorruptionTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsRecoveryAfterFileCorruptionTest.java
@@ -27,6 +27,7 @@
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheRebalanceMode;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
@@ -42,12 +43,14 @@
 import org.apache.ignite.internal.pagemem.store.PageStore;
 import org.apache.ignite.internal.pagemem.wal.IgniteWriteAheadLogManager;
 import org.apache.ignite.internal.pagemem.wal.record.CheckpointRecord;
+import org.apache.ignite.internal.pagemem.wal.record.RolloverType;
 import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
 import org.apache.ignite.internal.processors.cache.persistence.file.FileIO;
 import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStore;
 import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager;
 import org.apache.ignite.internal.processors.cache.persistence.pagemem.PageMemoryImpl;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
+import org.apache.ignite.internal.processors.cache.persistence.wal.FileWriteAheadLogManager;
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -55,6 +58,8 @@
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+
 /**
  * This test generates WAL & Page Store with N pages, then rewrites pages with zeroes and tries to acquire all pages.
  */
@@ -91,7 +96,7 @@
                     .setPersistenceEnabled(true)
                     .setName(policyName))
             .setWalMode(WALMode.LOG_ONLY)
-            .setCheckpointFrequency(500)
+            .setWalSegmentSize(1024 * 1024)
             .setAlwaysWriteFullPages(true);
 
         cfg.setDataStorageConfiguration(memCfg);
@@ -119,8 +124,7 @@
     @Test
     public void testPageRecoveryAfterFileCorruption() throws Exception {
         IgniteEx ig = startGrid(0);
-
-        ig.cluster().active(true);
+        ig.cluster().state(ClusterState.ACTIVE);
 
         IgniteCache<Integer, Integer> cache = ig.cache(cacheName);
 
@@ -175,8 +179,38 @@
         stopAllGrids();
 
         ig = startGrid(0);
+        ig.cluster().state(ClusterState.ACTIVE);
 
-        ig.cluster().active(true);
+        checkRestore(ig, pages);
+
+        // It is necessary to clear the current WAL history to make sure that the restored pages have been saved.
+        GridCacheSharedContext<Object, Object> cctx = ig.context().cache().context();
+        GridCacheDatabaseSharedManager dbMgr = (GridCacheDatabaseSharedManager)cctx.database();
+        FileWriteAheadLogManager wal = (FileWriteAheadLogManager)cctx.wal();
+
+        // Force checkpoint.
+        dbMgr.enableCheckpoints(true).get(getTestTimeout());
+
+        dbMgr.checkpointReadLock();
+
+        try {
+            WALPointer lastWalPtr = dbMgr.checkpointHistory().lastCheckpoint().checkpointMark();
+
+            // Move current WAL segment into the archive.
+            wal.log(new CheckpointRecord(null), RolloverType.NEXT_SEGMENT);
+            assertTrue(waitForCondition(() -> wal.lastArchivedSegment() >= lastWalPtr.index(), getTestTimeout()));
+
+            wal.truncate(lastWalPtr);
+            dbMgr.onWalTruncated(lastWalPtr);
+        }
+        finally {
+            dbMgr.checkpointReadUnlock();
+        }
+
+        stopAllGrids();
+
+        ig = startGrid(0);
+        ig.cluster().state(ClusterState.ACTIVE);
 
         checkRestore(ig, pages);
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsSporadicDataRecordsOnBackupTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsSporadicDataRecordsOnBackupTest.java
index 55b6b61..96899ad 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsSporadicDataRecordsOnBackupTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsSporadicDataRecordsOnBackupTest.java
@@ -21,6 +21,7 @@
 import java.util.Collections;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
@@ -34,6 +35,7 @@
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.pagemem.wal.WALIterator;
+import org.apache.ignite.internal.pagemem.wal.record.DataEntry;
 import org.apache.ignite.internal.pagemem.wal.record.DataRecord;
 import org.apache.ignite.internal.pagemem.wal.record.WALRecord;
 import org.apache.ignite.internal.processors.cache.GridCacheOperation;
@@ -172,11 +174,13 @@
 
                 DataRecord rec = (DataRecord)walEntry.get2();
 
-                createOpCnt += rec.writeEntries()
-                    .stream()
-                    .filter(e ->
-                        e.cacheId() == cacheId && GridCacheOperation.CREATE == e.op() && e.nearXidVersion() == null)
-                    .count();
+                Predicate<DataEntry> filter =
+                    e -> e.cacheId() == cacheId && GridCacheOperation.CREATE == e.op() && e.nearXidVersion() == null;
+
+                for (int i = 0; i < rec.entryCount(); i++) {
+                    if (filter.test(rec.get(i)))
+                        createOpCnt++;
+                }
             }
         }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/file/IgnitePdsCheckpointSimulationWithRealCpDisabledTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/file/IgnitePdsCheckpointSimulationWithRealCpDisabledTest.java
index 58bdef6..5bca3ee 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/file/IgnitePdsCheckpointSimulationWithRealCpDisabledTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/file/IgnitePdsCheckpointSimulationWithRealCpDisabledTest.java
@@ -413,9 +413,9 @@
 
                 DataEntry entry = entries.get(idx);
 
-                assertEquals(1, dataRec.writeEntries().size());
+                assertEquals(1, dataRec.entryCount());
 
-                DataEntry readEntry = dataRec.writeEntries().get(0);
+                DataEntry readEntry = dataRec.get(0);
 
                 assertEquals(entry.cacheId(), readEntry.cacheId());
                 assertEquals(entry.key().<Integer>value(coctx, true), readEntry.key().<Integer>value(coctx, true));
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRebalanceTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRebalanceTest.java
index 2d72c89..4def727 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRebalanceTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRebalanceTest.java
@@ -37,6 +37,7 @@
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.Ignition;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cache.CacheRebalanceMode;
@@ -87,6 +88,7 @@
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiInClosure;
 import org.apache.ignite.lang.IgniteBiPredicate;
+import org.apache.ignite.lang.IgniteCallable;
 import org.apache.ignite.lang.IgniteInClosure;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.plugin.extensions.communication.Message;
@@ -95,7 +97,6 @@
 import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -598,9 +599,10 @@
         // Fill initial data and force checkpoint.
         final int entryCnt = PARTS_CNT * 200;
         final int preloadEntryCnt = PARTS_CNT * 201;
+        int val = 0;
 
         for (int k = 0; k < preloadEntryCnt; k++)
-            cache0.put(k, new IndexedObject(k));
+            cache0.put(k, new IndexedObject(val++));
 
         forceCheckpoint();
 
@@ -614,14 +616,14 @@
             // This fact allows moving partitions to OWNING state during rebalancing
             // even though the corresponding RebalanceFuture will be cancelled.
             if (grid(0).affinity(cacheName).partition(k) != 12)
-                cache0.put(k, new IndexedObject(k));
+                cache0.put(k, new IndexedObject(val++));
         }
 
         // Upload additional data to a particular partition (primary partition belongs to coordinator, for instance)
         // in order to trigger full rebalance for that partition instead of historical one.
         int[] primaries0 = grid(0).affinity(cacheName).primaryPartitions(grid(0).localNode());
         for (int i = 0; i < preloadEntryCnt; ++i)
-            cache0.put(primaries0[0], new IndexedObject(primaries0[0]));
+            cache0.put(primaries0[0], new IndexedObject(val++));
 
         forceCheckpoint();
 
@@ -745,10 +747,113 @@
      *
      * @throws Exception If failed.
      */
-    @Ignore("https://issues.apache.org/jira/browse/IGNITE-15364")
     @Test
     public void testSwitchHistoricalRebalanceToFullAndClientJoin() throws Exception {
-        testSwitchHistoricalRebalanceToFull(IgniteWalRebalanceTest::injectFailingIOFactory, true);
+        testSwitchHistoricalRebalanceToFull(
+            IgniteWalRebalanceTest::injectFailingIOFactory,
+            () -> {
+                startClientGrid(G.allGrids().size());
+
+                return true;
+            });
+    }
+
+    /**
+     * Tests that demander switches to full rebalance if the previously chosen supplier for a group has failed
+     * to perform historical rebalance due to an unexpected error while historical iterator (wal iterator) is created.
+     * Additionally, the client node with a new cache joins the cluster between the demand message sent,
+     * and the supply message received.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSwitchHistoricalRebalanceToFullAndClientWithCacheJoin() throws Exception {
+        testSwitchHistoricalRebalanceToFull(
+            IgniteWalRebalanceTest::injectFailingIOFactory,
+            () -> {
+                String igniteInstanceName = getTestIgniteInstanceName(G.allGrids().size());
+
+                IgniteConfiguration cfg = optimize(getConfiguration(igniteInstanceName));
+
+                cfg.setClientMode(true);
+
+                CacheConfiguration<Object, Object> ccfg = new CacheConfiguration<>("test-client-cache")
+                    .setAtomicityMode(CacheAtomicityMode.ATOMIC)
+                    .setRebalanceMode(CacheRebalanceMode.ASYNC)
+                    .setCacheMode(CacheMode.PARTITIONED)
+                    .setBackups(backups)
+                    .setAffinity(new RendezvousAffinityFunction(false, PARTS_CNT));
+
+                cfg.setCacheConfiguration(ccfg);
+
+                startGrid(igniteInstanceName, cfg, null);
+
+                return false;
+            });
+    }
+
+    /**
+     * Tests that demander switches to full rebalance if the previously chosen supplier for a group has failed
+     * to perform historical rebalance due to an unexpected error while historical iterator (wal iterator) is created.
+     * Additionally, a new cache is created between the demand message sent, and the supply message received.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSwitchHistoricalRebalanceToFullAndStartNewCache() throws Exception {
+        testSwitchHistoricalRebalanceToFull(
+            IgniteWalRebalanceTest::injectFailingIOFactory,
+            () -> {
+                grid(0).getOrCreateCache(
+                    new CacheConfiguration<>("test-cache-3")
+                        .setAffinity(new RendezvousAffinityFunction(false, PARTS_CNT))
+                        .setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC)
+                        .setRebalanceOrder(30)
+                        .setBackups(1)
+                );
+
+                return false;
+            });
+    }
+
+    /**
+     * Tests that demander switches to full rebalance if the previously chosen supplier for a group has failed
+     * to perform historical rebalance due to an unexpected error while historical iterator (wal iterator) is created.
+     * Additionally, an existing cache is destroyed between the demand message sent, and the supply message received.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSwitchHistoricalRebalanceToFullAndDestroyCache() throws Exception {
+        testSwitchHistoricalRebalanceToFull(
+            IgniteWalRebalanceTest::injectFailingIOFactory,
+            () -> {
+                grid(0).cache("cache").destroy();
+
+                return false;
+            });
+    }
+
+    /**
+     * Tests that demander switches to full rebalance if the previously chosen supplier for a group has failed
+     * to perform historical rebalance due to an unexpected error while historical iterator (wal iterator) is created.
+     * Additionally, the server node left the cluster between the demand message sent, and the supply message received.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSwitchHistoricalRebalanceToFullAndStopBaselineNode() throws Exception {
+        backups = 3;
+
+        IgniteEx justNode = startGrid(3);
+
+        testSwitchHistoricalRebalanceToFull(
+            IgniteWalRebalanceTest::injectFailingIOFactory,
+            () -> {
+                Ignition.stop(justNode.name(), true);
+
+                return false;
+            });
     }
 
     /**
@@ -759,7 +864,7 @@
      */
     @Test
     public void testSwitchHistoricalRebalanceToFullDueToFailOnCreatingWalIterator() throws Exception {
-        testSwitchHistoricalRebalanceToFull(IgniteWalRebalanceTest::injectFailingIOFactory, false);
+        testSwitchHistoricalRebalanceToFull(IgniteWalRebalanceTest::injectFailingIOFactory, () -> true);
     }
 
     /**
@@ -770,41 +875,43 @@
      */
     @Test
     public void testSwitchHistoricalRebalanceToFullWhileIteratingOverWAL() throws Exception {
-        testSwitchHistoricalRebalanceToFull(supplier1 -> {
-            try {
-                // Corrupt wal record in order to fail historical rebalance from supplier1 node.
-                IgniteWriteAheadLogManager walMgr = supplier1.context().cache().context().wal();
+        testSwitchHistoricalRebalanceToFull(
+            supplier1 -> {
+                try {
+                    // Corrupt wal record in order to fail historical rebalance from supplier1 node.
+                    IgniteWriteAheadLogManager walMgr = supplier1.context().cache().context().wal();
 
-                WALPointer ptr = walMgr.log(new DataRecord(new DataEntry(
-                    CU.cacheId("test-cache-1"),
-                    new KeyCacheObjectImpl(0, null, 0),
-                    null,
-                    GridCacheOperation.DELETE,
-                    new GridCacheVersion(0, 1, 1, 0),
-                    new GridCacheVersion(0, 1, 1, 0),
-                    0,
-                    0,
-                    0,
-                    DataEntry.EMPTY_FLAGS
-                )));
+                    WALPointer ptr = walMgr.log(new DataRecord(new DataEntry(
+                        CU.cacheId("test-cache-1"),
+                        new KeyCacheObjectImpl(0, null, 0),
+                        null,
+                        GridCacheOperation.DELETE,
+                        new GridCacheVersion(0, 1, 1, 0),
+                        new GridCacheVersion(0, 1, 1, 0),
+                        0,
+                        0,
+                        0,
+                        DataEntry.EMPTY_FLAGS
+                    )));
 
-                File walDir = U.field(walMgr, "walWorkDir");
+                    File walDir = U.field(walMgr, "walWorkDir");
 
-                List<FileDescriptor> walFiles = new IgniteWalIteratorFactory().resolveWalFiles(
-                    new IgniteWalIteratorFactory.IteratorParametersBuilder().filesOrDirs(walDir));
+                    List<FileDescriptor> walFiles = new IgniteWalIteratorFactory().resolveWalFiles(
+                        new IgniteWalIteratorFactory.IteratorParametersBuilder().filesOrDirs(walDir));
 
-                FileDescriptor lastWalFile = walFiles.get(walFiles.size() - 1);
+                    FileDescriptor lastWalFile = walFiles.get(walFiles.size() - 1);
 
-                WalTestUtils.corruptWalSegmentFile(lastWalFile, ptr);
+                    WalTestUtils.corruptWalSegmentFile(lastWalFile, ptr);
 
-                IgniteCache<Integer, IndexedObject> c1 = supplier1.cache("test-cache-1");
-                for (int i = 0; i < PARTS_CNT * 100; i++)
-                    c1.put(i, new IndexedObject(i));
-            }
-            catch (IgniteCheckedException | IOException e) {
-                throw new RuntimeException(e);
-            }
-        }, false);
+                    IgniteCache<Integer, IndexedObject> c1 = supplier1.cache("test-cache-1");
+                    for (int i = 0; i < PARTS_CNT * 100; i++)
+                        c1.put(i, new IndexedObject(i + PARTS_CNT));
+                }
+                catch (IgniteCheckedException | IOException e) {
+                    throw new RuntimeException(e);
+                }
+            },
+            () -> true);
     }
 
     /**
@@ -812,13 +919,13 @@
      * to perform historical rebalance due to an unexpected error.
      *
      * @param corruptWalClo Closure that corrupts wal iterating on supplier node.
-     * @param needClientStart {@code true} if client node should join the cluster between
-     *                                    the demand message sent and the supply message received.
+     * @param clientClo Closure that is called between the demand message sent and the supply message received.
+     *                  Returns {@code true} if it is assumed that the rebalancing from the second supplier should be reassigned.
      * @throws Exception If failed
      */
     public void testSwitchHistoricalRebalanceToFull(
         IgniteInClosure<IgniteEx> corruptWalClo,
-        boolean needClientStart
+        IgniteCallable<Boolean> clientClo
     ) throws Exception {
         backups = 3;
 
@@ -856,11 +963,12 @@
         // Fill initial data.
         final int entryCnt = PARTS_CNT * 200;
         final int preloadEntryCnt = PARTS_CNT * 400;
+        int val = 0;
 
         for (int k = 0; k < preloadEntryCnt; k++) {
-            c1.put(k, new IndexedObject(k));
+            c1.put(k, new IndexedObject(val++));
 
-            c2.put(k, new IndexedObject(k));
+            c2.put(k, new IndexedObject(val++));
         }
 
         forceCheckpoint();
@@ -869,9 +977,9 @@
 
         // Rewrite data to trigger further rebalance.
         for (int i = 0; i < entryCnt; i++) {
-            c1.put(i, new IndexedObject(i));
+            c1.put(i, new IndexedObject(val++));
 
-            c2.put(i, new IndexedObject(i));
+            c2.put(i, new IndexedObject(val++));
         }
 
         // Delay rebalance process for specified groups.
@@ -935,14 +1043,15 @@
         final IgniteInternalFuture<Boolean> preloadFut2 = restartedDemander.cachex(cacheName2).context().group()
             .preloader().rebalanceFuture();
 
-        if (needClientStart)
-            startClientGrid(3);
+        boolean rebalanceReassigned = clientClo.call();
 
         // Unblock messages and start tracking demand and supply messages.
         demanderSpi.stopBlock();
 
-        // Wait until rebalancing will be cancelled for both suppliers.
-        GridTestUtils.waitForCondition(() -> preloadFut1.isDone() && preloadFut2.isDone(), getTestTimeout());
+        // Wait until rebalancing will be cancelled.
+        GridTestUtils.waitForCondition(
+            () -> preloadFut1.isDone() && (!rebalanceReassigned || (rebalanceReassigned && preloadFut2.isDone())),
+            getTestTimeout());
 
         Assert.assertEquals(
             "Rebalance should be cancelled on demander node: " + preloadFut1,
@@ -951,7 +1060,7 @@
         Assert.assertEquals(
             "Rebalance should be cancelled on demander node: " + preloadFut2,
             false,
-            preloadFut2.get());
+            rebalanceReassigned && preloadFut2.get());
 
         // Unblock supply messages from supplier2
         supplierSpi2.stopBlock();
@@ -992,11 +1101,21 @@
             .filter(msg -> msg.hasFull() || msg.hasHistorical())
             .collect(toList());
 
-        assertEquals("There should only two demand messages.", 2, demandMsgsForSupplier2.size());
-        assertTrue(
-            "Both messages should require historical rebalance [" +
-                "msg=" + demandMsgsForSupplier2.get(0) + ", msg=" + demandMsgsForSupplier2.get(1) + ']',
+        if (rebalanceReassigned) {
+            assertEquals(
+                "There should be only two demand messages.", 2, demandMsgsForSupplier2.size());
+            assertTrue(
+                "Both messages should require historical rebalance [" +
+                    "msg=" + demandMsgsForSupplier2.get(0) + ", msg=" + demandMsgsForSupplier2.get(1) + ']',
                 histPred.apply(demandMsgsForSupplier2.get(0)) && histPred.apply(demandMsgsForSupplier2.get(1)));
+        }
+        else {
+            assertEquals(
+                "There should be only one demand message.", 1, demandMsgsForSupplier2.size());
+            assertTrue(
+                "Message should require historical rebalance [" + "msg=" + demandMsgsForSupplier2.get(0) + ']',
+                histPred.apply(demandMsgsForSupplier2.get(0)));
+        }
     }
 
     /**
@@ -1035,11 +1154,12 @@
         // Fill initial data.
         final int entryCnt = PARTS_CNT * 200;
         final int preloadEntryCnt = PARTS_CNT * 400;
+        int val = 0;
 
         for (int k = 0; k < preloadEntryCnt; k++) {
-            c1.put(k, new IndexedObject(k));
+            c1.put(k, new IndexedObject(val++));
 
-            c2.put(k, new IndexedObject(k));
+            c2.put(k, new IndexedObject(val++));
         }
 
         forceCheckpoint();
@@ -1051,11 +1171,11 @@
         // Updating entryCnt keys allows to trigger historical rebalance.
         // This is an easy way to emulate missing partitions on the first rebalance.
         for (int i = 0; i < entryCnt; i++)
-            c1.put(i, new IndexedObject(i));
+            c1.put(i, new IndexedObject(val++));
 
         // Full rebalance for the cacheName2.
         for (int i = 0; i < preloadEntryCnt; i++)
-            c2.put(i, new IndexedObject(i));
+            c2.put(i, new IndexedObject(val++));
 
         // Delay rebalance process for specified groups.
         blockMsgPred = (node, msg) -> {
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/reader/IgniteWalReaderTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/reader/IgniteWalReaderTest.java
index 64a521d..79278e6 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/reader/IgniteWalReaderTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/reader/IgniteWalReaderTest.java
@@ -279,7 +279,8 @@
                 if (walRecord.type() == DATA_RECORD_V2 || walRecord.type() == MVCC_DATA_RECORD) {
                     DataRecord record = (DataRecord)walRecord;
 
-                    for (DataEntry entry : record.writeEntries()) {
+                    for (int i = 0; i < record.entryCount(); i++) {
+                        DataEntry entry = record.get(i);
                         KeyCacheObject key = entry.key();
                         CacheObject val = entry.value();
 
@@ -1019,11 +1020,11 @@
             0,
             null,
             dataRecord -> {
-                final List<DataEntry> entries = dataRecord.writeEntries();
-
                 sb.append("{");
 
-                for (DataEntry entry : entries) {
+                for (int i = 0; i < dataRecord.entryCount(); i++) {
+                    DataEntry entry = dataRecord.get(i);
+
                     GridCacheOperation op = entry.op();
                     Integer cnt = operationsFound.get(op);
 
@@ -1102,9 +1103,9 @@
         Map<GridCacheOperation, Integer> operationsFound = new EnumMap<>(GridCacheOperation.class);
 
         IgniteInClosure<DataRecord> drHnd = dataRecord -> {
-            List<? extends DataEntry> entries = dataRecord.writeEntries();
+            for (int i = 0; i < dataRecord.entryCount(); i++) {
+                DataEntry entry = dataRecord.get(i);
 
-            for (DataEntry entry : entries) {
                 GridCacheOperation op = entry.op();
                 Integer cnt = operationsFound.get(op);
 
@@ -1189,9 +1190,9 @@
         Map<GridCacheOperation, Integer> operationsFound = new EnumMap<>(GridCacheOperation.class);
 
         IgniteInClosure<DataRecord> drHnd = dataRecord -> {
-            List<? extends DataEntry> entries = dataRecord.writeEntries();
+            for (int i = 0; i < dataRecord.entryCount(); i++) {
+                DataEntry entry = dataRecord.get(i);
 
-            for (DataEntry entry : entries) {
                 GridCacheOperation op = entry.op();
                 Integer cnt = operationsFound.get(op);
 
@@ -1279,11 +1280,11 @@
         Map<GridCacheOperation, Integer> operationsFound = new EnumMap<>(GridCacheOperation.class);
 
         IgniteInClosure<DataRecord> drHnd = dataRecord -> {
-            List<DataEntry> entries = dataRecord.writeEntries();
-
             sb.append("{");
 
-            for (DataEntry entry : entries) {
+            for (int i = 0; i < dataRecord.entryCount(); i++) {
+                DataEntry entry = dataRecord.get(i);
+
                 GridCacheOperation op = entry.op();
                 Integer cnt = operationsFound.get(op);
 
@@ -1607,9 +1608,9 @@
                         if (dataRecordHnd != null)
                             dataRecordHnd.apply(dataRecord);
 
-                        List<DataEntry> entries = dataRecord.writeEntries();
+                        for (int i = 0; i < dataRecord.entryCount(); i++) {
+                            DataEntry entry = dataRecord.get(i);
 
-                        for (DataEntry entry : entries) {
                             if (walRecord.type() == DATA_RECORD_V2) {
                                 assertEquals(primary, (entry.flags() & DataEntry.PRIMARY_FLAG) != 0);
                                 assertEquals(rebalance, (entry.flags() & DataEntry.PRELOAD_FLAG) != 0);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreSelfTest.java
index b6361e1..af70b8e 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreSelfTest.java
@@ -101,6 +101,22 @@
     }
 
     /**
+     * Checks snapshot restore if not all "affinity" partitions have been physically created.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRestoreWithEmptyPartitions() throws Exception {
+        int keysCnt = dfltCacheCfg.getAffinity().partitions() / 2;
+
+        Ignite ignite = startGridsWithSnapshot(1, keysCnt, false);
+
+        ignite.snapshot().restoreSnapshot(SNAPSHOT_NAME, null).get(TIMEOUT);
+
+        assertCacheKeys(ignite.cache(DEFAULT_CACHE_NAME), keysCnt);
+    }
+
+    /**
      * Ensures that system partition verification task is invoked before restoring the snapshot.
      *
      * @throws Exception If failed.
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotSelfTest.java
index 3a2e031..fbff428 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotSelfTest.java
@@ -43,6 +43,7 @@
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.Ignition;
 import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.cache.query.ScanQuery;
 import org.apache.ignite.cluster.ClusterTopologyException;
 import org.apache.ignite.configuration.CacheConfiguration;
@@ -107,7 +108,8 @@
     /** Cache configuration for test. */
     private static final CacheConfiguration<Integer, Integer> atomicCcfg = new CacheConfiguration<Integer, Integer>("atomicCacheName")
         .setAtomicityMode(CacheAtomicityMode.ATOMIC)
-        .setBackups(2);
+        .setBackups(2)
+        .setAffinity(new RendezvousAffinityFunction(false, CACHE_PARTITIONS_COUNT));
 
     /** {@code true} if node should be started in separate jvm. */
     protected volatile boolean jvm;
@@ -581,7 +583,7 @@
 
         IgniteFuture<?> fut = ignite.snapshot().createSnapshot(SNAPSHOT_NAME);
 
-        U.await(partProcessed);
+        U.await(partProcessed, TIMEOUT, TimeUnit.MILLISECONDS);
 
         stopGrid(1);
 
@@ -1191,7 +1193,7 @@
                 started.countDown();
 
                 try {
-                    U.await(blocked);
+                    U.await(blocked, TIMEOUT, TimeUnit.MILLISECONDS);
 
                     if (log.isInfoEnabled())
                         log.info("Latch released. Processing delta file continued: " + delta.getName());
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotMXBeanTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotMXBeanTest.java
index c662a5a..22d0ff0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotMXBeanTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotMXBeanTest.java
@@ -22,19 +22,30 @@
 import javax.management.DynamicMBean;
 import javax.management.MBeanException;
 import javax.management.ReflectionException;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.TestRecordingCommunicationSpi;
+import org.apache.ignite.internal.util.distributed.SingleNodeMessage;
+import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.mxbean.SnapshotMXBean;
 import org.apache.ignite.spi.metric.jmx.JmxMetricExporterSpi;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
 
 import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.SNAPSHOT_METRICS;
+import static org.apache.ignite.internal.processors.cache.persistence.snapshot.SnapshotRestoreProcess.SNAPSHOT_RESTORE_METRICS;
+import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.RESTORE_CACHE_GROUP_SNAPSHOT_PREPARE;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
 
 /**
  * Tests {@link SnapshotMXBean}.
  */
 public class IgniteSnapshotMXBeanTest extends AbstractSnapshotSelfTest {
+    /** Metric group name. */
+    private static final String METRIC_GROUP = "Snapshot";
+
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         return super.getConfiguration(igniteInstanceName)
@@ -49,14 +60,14 @@
         DynamicMBean snpMBean = metricRegistry(ignite.name(), null, SNAPSHOT_METRICS);
 
         assertEquals("Snapshot end time must be undefined on first snapshot operation starts.",
-            0, getLastSnapshotEndTime(snpMBean));
+            0, (long)getMetric("LastSnapshotEndTime", snpMBean));
 
-        SnapshotMXBean mxBean = getMxBean(ignite.name(), "Snapshot", SnapshotMXBeanImpl.class, SnapshotMXBean.class);
+        SnapshotMXBean mxBean = getMxBean(ignite.name(), METRIC_GROUP, SnapshotMXBeanImpl.class, SnapshotMXBean.class);
 
         mxBean.createSnapshot(SNAPSHOT_NAME);
 
         assertTrue("Waiting for snapshot operation failed.",
-            GridTestUtils.waitForCondition(() -> getLastSnapshotEndTime(snpMBean) > 0, 10_000));
+            GridTestUtils.waitForCondition(() -> (long)getMetric("LastSnapshotEndTime", snpMBean) > 0, TIMEOUT));
 
         stopAllGrids();
 
@@ -72,7 +83,7 @@
         IgniteEx startCli = startClientGrid(1);
         IgniteEx killCli = startClientGrid(2);
 
-        SnapshotMXBean mxBean = getMxBean(killCli.name(), "Snapshot", SnapshotMXBeanImpl.class,
+        SnapshotMXBean mxBean = getMxBean(killCli.name(), METRIC_GROUP, SnapshotMXBeanImpl.class,
             SnapshotMXBean.class);
 
         doSnapshotCancellationTest(startCli,
@@ -81,14 +92,89 @@
             mxBean::cancelSnapshot);
     }
 
-    /**
+    /** @throws Exception If fails. */
+    @Test
+    public void testRestoreSnapshot() throws Exception {
+        // TODO IGNITE-14999 Support dynamic restoration of encrypted snapshots.
+        if (encryption)
+            return;
 
-     * @param mBean Ignite snapshot MBean.
-     * @return Value of snapshot end time.
+        IgniteEx ignite = startGridsWithSnapshot(2, CACHE_KEYS_RANGE, false);
+
+        DynamicMBean mReg0 = metricRegistry(grid(0).name(), null, SNAPSHOT_RESTORE_METRICS);
+        DynamicMBean mReg1 = metricRegistry(grid(1).name(), null, SNAPSHOT_RESTORE_METRICS);
+
+        assertEquals(0, (long)getMetric("endTime", mReg0));
+        assertEquals(0, (long)getMetric("endTime", mReg1));
+
+        getMxBean(ignite.name(), METRIC_GROUP, SnapshotMXBeanImpl.class, SnapshotMXBean.class)
+            .restoreSnapshot(SNAPSHOT_NAME, null);
+
+        assertTrue(GridTestUtils.waitForCondition(() -> (long)getMetric("endTime", mReg0) > 0, TIMEOUT));
+        assertTrue(GridTestUtils.waitForCondition(() -> (long)getMetric("endTime", mReg1) > 0, TIMEOUT));
+
+        assertCacheKeys(ignite.cache(DEFAULT_CACHE_NAME), CACHE_KEYS_RANGE);
+    }
+
+    /** @throws Exception If fails. */
+    @Test
+    public void testCancelRestoreSnapshot() throws Exception {
+        // TODO IGNITE-14999 Support dynamic restoration of encrypted snapshots.
+        if (encryption)
+            return;
+
+        IgniteEx ignite = startGridsWithSnapshot(2, CACHE_KEYS_RANGE, false);
+        SnapshotMXBean mxBean = getMxBean(ignite.name(), METRIC_GROUP, SnapshotMXBeanImpl.class, SnapshotMXBean.class);
+        DynamicMBean mReg0 = metricRegistry(grid(0).name(), null, SNAPSHOT_RESTORE_METRICS);
+        DynamicMBean mReg1 = metricRegistry(grid(1).name(), null, SNAPSHOT_RESTORE_METRICS);
+
+        assertEquals("", getMetric("error", mReg0));
+        assertEquals("", getMetric("error", mReg1));
+        assertEquals(0, (long)getMetric("endTime", mReg0));
+        assertEquals(0, (long)getMetric("endTime", mReg1));
+
+        TestRecordingCommunicationSpi spi = TestRecordingCommunicationSpi.spi(grid(1));
+
+        spi.blockMessages((node, msg) -> msg instanceof SingleNodeMessage &&
+            ((SingleNodeMessage<?>)msg).type() == RESTORE_CACHE_GROUP_SNAPSHOT_PREPARE.ordinal());
+
+        IgniteFuture<Void> fut = ignite.snapshot().restoreSnapshot(SNAPSHOT_NAME, null);
+
+        spi.waitForBlocked();
+
+       IgniteInternalFuture<Boolean> interruptFut = GridTestUtils.runAsync(() -> {
+            try {
+                return GridTestUtils.waitForCondition(
+                    () -> !"".equals(getMetric("error", mReg0)) && !"".equals(getMetric("error", mReg1)), TIMEOUT);
+            } finally {
+                spi.stopBlock();
+            }
+        });
+
+        mxBean.cancelSnapshotRestore(SNAPSHOT_NAME);
+
+        assertTrue(interruptFut.get());
+
+        String expErrMsg = "Operation has been canceled by the user.";
+
+        assertThrowsAnyCause(log, () -> fut.get(TIMEOUT), IgniteCheckedException.class, expErrMsg);
+
+        assertTrue((long)getMetric("endTime", mReg0) > 0);
+        assertTrue((long)getMetric("endTime", mReg1) > 0);
+        assertTrue(((String)getMetric("error", mReg0)).contains(expErrMsg));
+        assertTrue(((String)getMetric("error", mReg1)).contains(expErrMsg));
+
+        assertNull(ignite.cache(DEFAULT_CACHE_NAME));
+    }
+
+    /**
+     * @param mBean Ignite snapshot restore MBean.
+     * @param name Metric name.
+     * @return Metric value.
      */
-    private static long getLastSnapshotEndTime(DynamicMBean mBean) {
+    private static <T> T getMetric(String name, DynamicMBean mBean) {
         try {
-            return (long)mBean.getAttribute("LastSnapshotEndTime");
+            return (T)mBean.getAttribute(name);
         }
         catch (MBeanException | ReflectionException | AttributeNotFoundException e) {
             throw new RuntimeException(e);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManagerSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManagerSelfTest.java
index dd83af1..9a1ec1a 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManagerSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManagerSelfTest.java
@@ -19,6 +19,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadMXBean;
 import java.nio.ByteBuffer;
 import java.nio.file.OpenOption;
 import java.util.Collections;
@@ -31,6 +33,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiFunction;
+import java.util.stream.LongStream;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
@@ -64,8 +67,10 @@
 import org.apache.ignite.testframework.LogListener;
 import org.junit.Test;
 
+import static java.util.Objects.nonNull;
 import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
 import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.CP_SNAPSHOT_REASON;
+import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.SNAPSHOT_RUNNER_THREAD_PREFIX;
 import static org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
 import static org.apache.ignite.testframework.GridTestUtils.setFieldValue;
 
@@ -79,6 +84,9 @@
     /** Listenning logger. */
     private ListeningTestLogger listenLog;
 
+    /** Number of threads being used to perform snapshot operation. */
+    private Integer snapshotThreadPoolSize;
+
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
@@ -90,6 +98,9 @@
         if (listenLog != null)
             cfg.setGridLogger(listenLog);
 
+        if (nonNull(snapshotThreadPoolSize))
+            cfg.setSnapshotThreadPoolSize(snapshotThreadPoolSize);
+
         return cfg;
     }
 
@@ -539,6 +550,23 @@
         snpFut.get(testTimeout);
     }
 
+    /** @throws Exception If fails. */
+    @Test
+    public void testSnapshotThreadPoolSizeUsage() throws Exception {
+        snapshotThreadPoolSize = 6;
+
+        IgniteEx ig = startGridWithCache(dfltCacheCfg, CACHE_KEYS_RANGE);
+
+        ig.snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
+
+        ThreadMXBean tMb = ManagementFactory.getThreadMXBean();
+
+        long snpRunningThreads = LongStream.of(tMb.getAllThreadIds()).mapToObj(tMb::getThreadInfo)
+            .filter(info -> info.getThreadName().startsWith(SNAPSHOT_RUNNER_THREAD_PREFIX)).count();
+
+        assertEquals(snapshotThreadPoolSize.longValue(), snpRunningThreads);
+    }
+
     /**
      * @param ignite Ignite instance to set factory.
      * @param factory New factory to use.
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotRestoreFromRemoteTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotRestoreFromRemoteTest.java
index 56ef3c2..fa75b56 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotRestoreFromRemoteTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotRestoreFromRemoteTest.java
@@ -44,6 +44,8 @@
 import org.apache.ignite.internal.TestRecordingCommunicationSpi;
 import org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionDemandMessage;
 import org.apache.ignite.internal.processors.cache.persistence.partstate.GroupPartitionId;
+import org.apache.ignite.internal.processors.cache.verify.IdleVerifyResultV2;
+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.lang.IgniteFuture;
@@ -59,6 +61,7 @@
 import static org.apache.ignite.events.EventType.EVT_CLUSTER_SNAPSHOT_RESTORE_STARTED;
 import static org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager.partId;
 import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.resolveSnapshotWorkDirectory;
+import static org.apache.ignite.testframework.GridTestUtils.assertContains;
 
 /** */
 public class IgniteSnapshotRestoreFromRemoteTest extends IgniteClusterSnapshotRestoreBaseTest {
@@ -178,6 +181,44 @@
 
     /** @throws Exception If failed. */
     @Test
+    public void testRestoreFromAnEmptyNode() throws Exception {
+        startDedicatedGrids(SECOND_CLUSTER_PREFIX, 2);
+
+        copyAndShuffle(snpParts, G.allGrids());
+
+        // Start a new node without snapshot working directory.
+        IgniteEx emptyNode = startDedicatedGrid(SECOND_CLUSTER_PREFIX, 2);
+
+        emptyNode.cluster().state(ClusterState.ACTIVE);
+
+        emptyNode.cache(DEFAULT_CACHE_NAME).destroy();
+        awaitPartitionMapExchange();
+
+        // Ensure that the snapshot check command succeeds.
+        IdleVerifyResultV2 res =
+            emptyNode.context().cache().context().snapshotMgr().checkSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
+
+        StringBuilder buf = new StringBuilder();
+        res.print(buf::append, true);
+
+        assertTrue(F.isEmpty(res.exceptions()));
+        assertPartitionsSame(res);
+        assertContains(log, buf.toString(), "The check procedure has finished, no conflicts have been found");
+
+        // Restore all cache groups.
+        emptyNode.snapshot().restoreSnapshot(SNAPSHOT_NAME, null).get(TIMEOUT);
+
+        awaitPartitionMapExchange(true, true, null, true);
+
+        for (Ignite grid : G.allGrids()) {
+            assertCacheKeys(grid.cache(DEFAULT_CACHE_NAME), CACHE_KEYS_RANGE);
+            assertCacheKeys(grid.cache(CACHE1), CACHE_KEYS_RANGE);
+            assertCacheKeys(grid.cache(CACHE2), CACHE_KEYS_RANGE);
+        }
+    }
+
+    /** @throws Exception If failed. */
+    @Test
     public void testRestoreNoRebalance() throws Exception {
         IgniteEx scc = startDedicatedGrids(SECOND_CLUSTER_PREFIX, 2);
         scc.cluster().state(ClusterState.ACTIVE);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotWithMetastorageTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotWithMetastorageTest.java
index a376148..0c9df19 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotWithMetastorageTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotWithMetastorageTest.java
@@ -21,6 +21,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentSkipListSet;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -140,7 +141,7 @@
     public void testMetastorageUpdateOnSnapshotFail() throws Exception {
         AtomicInteger keyCnt = new AtomicInteger();
         AtomicBoolean stop = new AtomicBoolean();
-        Set<String> writtenKeys = new TreeSet<>();
+        Set<String> writtenKeys = new ConcurrentSkipListSet<>();
 
         IgniteEx ignite = startGridsWithCache(2, dfltCacheCfg, CACHE_KEYS_RANGE);
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIOTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIOTest.java
index e050353..73d972a 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIOTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIOTest.java
@@ -71,6 +71,8 @@
 
         buf.order(ByteOrder.nativeOrder());
 
+        PageIO.setType(GridUnsafe.bufferAddress(buf), PageIO.T_PAGE_UPDATE_TRACKING);
+
         return buf;
     }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIteratorTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIteratorTest.java
index b490543..fec0ffa 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIteratorTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneWalRecordsIteratorTest.java
@@ -48,6 +48,7 @@
 import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
 import org.apache.ignite.internal.util.lang.GridAbsPredicate;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteBiTuple;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.testframework.GridTestUtils;
@@ -97,6 +98,11 @@
      *
      */
     private String createWalFiles() throws Exception {
+        return createWalFiles(1);
+    }
+
+    /** */
+    private String createWalFiles(int segRecCnt) throws Exception {
         IgniteEx ig = (IgniteEx)startGrid();
 
         String archiveWalDir = getArchiveWalDirPath(ig);
@@ -112,7 +118,10 @@
             sharedMgr.checkpointReadLock();
 
             try {
-                walMgr.log(new SnapshotRecord(i, false), RolloverType.NEXT_SEGMENT);
+                for (int j = 0; j < segRecCnt - 1; j++)
+                    walMgr.log(new SnapshotRecord(i * segRecCnt + j, false));
+
+                walMgr.log(new SnapshotRecord(i * segRecCnt + segRecCnt - 1, false), RolloverType.NEXT_SEGMENT);
             }
             finally {
                 sharedMgr.checkpointReadUnlock();
@@ -144,6 +153,121 @@
         );
     }
 
+    /** */
+    @Test
+    public void testNoNextIfLowBoundInTheEnd() throws Exception {
+        String dir = createWalFiles(3);
+
+        WALIterator iter = createWalIterator(dir, null, null, false);
+
+        assertFalse(iter.lastRead().isPresent());
+        assertTrue(iter.hasNext());
+
+        while (iter.hasNext()) {
+            IgniteBiTuple<WALPointer, WALRecord> curr = iter.next();
+
+            assertEquals("Last read should point to the current record", curr.get1(), iter.lastRead().get());
+        }
+
+        iter.close();
+
+        iter = createWalIterator(dir, iter.lastRead().get().next(), null, false);
+
+        assertFalse(iter.lastRead().isPresent());
+
+        assertFalse(iter.hasNext());
+
+        iter.close();
+    }
+
+    /** */
+    @Test
+    public void testNextRecordReturnedForLowBounds() throws Exception {
+        String dir = createWalFiles(3);
+
+        WALIterator iter = createWalIterator(dir, null, null, false);
+
+        IgniteBiTuple<WALPointer, WALRecord> prev = iter.next();
+
+        assertEquals("Last read should point to the current record", prev.get1(), iter.lastRead().get());
+
+        iter.close();
+
+        iter = createWalIterator(dir, iter.lastRead().get().next(), null, false);
+
+        assertFalse(iter.lastRead().isPresent());
+        assertTrue(iter.hasNext());
+
+        while (iter.hasNext()) {
+            IgniteBiTuple<WALPointer, WALRecord> cur = iter.next();
+
+            assertEquals("Last read should point to the current record", cur.get1(), iter.lastRead().get());
+
+            assertFalse(
+                "Should read next record[prev=" + prev.get1() + ", cur=" + cur.get1() + ']',
+                prev.get1().equals(cur.get1())
+            );
+
+            prev = cur;
+
+            iter.close();
+
+            iter = createWalIterator(dir, iter.lastRead().get().next(), null, false);
+
+            assertFalse(iter.lastRead().isPresent());
+        }
+
+        iter.close();
+    }
+
+    /** */
+    @Test
+    public void testLastRecordFiltered() throws Exception {
+        String dir = createWalFiles();
+
+        WALIterator iter = createWalIterator(dir, null, null, false);
+
+        IgniteBiTuple<WALPointer, WALRecord> lastRec = null;
+
+        // Search for the last record.
+        while (iter.hasNext())
+            lastRec = iter.next();
+
+        iter.close();
+
+        assertNotNull(lastRec);
+
+        WALPointer lastPointer = iter.lastRead().get();
+
+        WALRecord.RecordType lastRecType = lastRec.get2().type();
+
+        // Iterating and filter out last record.
+        iter = createWalIterator(dir, null, null, false, (type, ptr) -> type != lastRecType);
+
+        assertTrue(iter.hasNext());
+
+        while (iter.hasNext()) {
+            lastRec = iter.next();
+
+            assertNotNull(lastRec.get2().type()); // Type is null for filtered records.
+
+            assertTrue(lastRec.get2().type() != lastRecType);
+        }
+
+        iter.close();
+
+        assertNotNull(lastRec);
+
+        assertEquals(
+            "LastRead should point to the last WAL Record even it filtered",
+            lastPointer,
+            iter.lastRead().get()
+        );
+
+        // Record on `lastPointer` is filtered so.
+        assertEquals("Last returned record should be before lastPointer", -1, lastRec.get1().compareTo(lastPointer));
+    }
+
     /**
      * Check correct check bounds.
      *
@@ -256,14 +380,29 @@
         return new IgniteWalIteratorFactory(log).resolveWalFiles(params.filesOrDirs(walDir));
     }
 
+    /** */
+    private WALIterator createWalIterator(
+        String walDir,
+        WALPointer lowBound,
+        WALPointer highBound,
+        boolean strictCheck
+    ) throws IgniteCheckedException {
+        return createWalIterator(walDir, lowBound, highBound, strictCheck, null);
+    }
+
     /**
      * @param walDir Wal directory.
      * @param lowBound Low bound.
      * @param highBound High bound.
      * @param strictCheck Strict check.
      */
-    private WALIterator createWalIterator(String walDir, WALPointer lowBound, WALPointer highBound, boolean strictCheck)
-                    throws IgniteCheckedException {
+    private WALIterator createWalIterator(
+        String walDir,
+        WALPointer lowBound,
+        WALPointer highBound,
+        boolean strictCheck,
+        IgniteBiPredicate<WALRecord.RecordType, WALPointer> filter
+    ) throws IgniteCheckedException {
         IteratorParametersBuilder params = new IteratorParametersBuilder();
 
         params.ioFactory(new RandomAccessFileIOFactory()).
@@ -273,9 +412,12 @@
         if (lowBound != null)
             params.from(lowBound);
 
-        if (lowBound != null)
+        if (highBound != null)
             params.to(highBound);
 
+        if (filter != null)
+            params.filter(filter);
+
         return new IgniteWalIteratorFactory(log).iterator(params);
     }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/transactions/TxPartitionCounterStateConsistencyTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/transactions/TxPartitionCounterStateConsistencyTest.java
index dbd45d5..6d9b5ae 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/transactions/TxPartitionCounterStateConsistencyTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/transactions/TxPartitionCounterStateConsistencyTest.java
@@ -1381,9 +1381,9 @@
 
                 DataRecord rec = (DataRecord)tup.get2();
 
-                assertEquals(1, rec.writeEntries().size());
+                assertEquals(1, rec.entryCount());
 
-                DataEntry entry = rec.writeEntries().get(0);
+                DataEntry entry = rec.get(0);
 
                 assertEquals(op.get1(),
                     entry.key().value(internalCache(ig, DEFAULT_CACHE_NAME).context().cacheObjectContext(), false));
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cluster/ClusterReadOnlyModeSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cluster/ClusterReadOnlyModeSelfTest.java
index d390dc0..da360ad 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cluster/ClusterReadOnlyModeSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cluster/ClusterReadOnlyModeSelfTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.processors.cluster;
 
+import java.util.HashSet;
 import java.util.concurrent.CountDownLatch;
 import java.util.stream.Stream;
 import javax.cache.CacheException;
@@ -28,10 +29,10 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.failure.StopNodeFailureHandler;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.GridCacheUtilityKey;
 import org.apache.ignite.internal.processors.cache.distributed.dht.IgniteClusterReadOnlyException;
 import org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
 import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage;
-import org.apache.ignite.internal.processors.service.GridServiceAssignmentsKey;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -257,9 +258,22 @@
 
         checkClusterInReadOnlyMode(true, grid);
 
-        grid.utilityCache().put(new GridServiceAssignmentsKey("test"), "test");
+        GridCacheUtilityKey<?> key = new GridCacheUtilityKey() {
+            @Override protected boolean equalsx(GridCacheUtilityKey key) {
+                return false;
+            }
 
-        assertEquals("test", grid.utilityCache().get(new GridServiceAssignmentsKey("test")));
+            @Override public int hashCode() {
+                return 0;
+            }
+        };
+
+        HashSet<String> sysTypes = GridTestUtils.getFieldValue(grid.context().marshallerContext(), "sysTypesSet");
+        sysTypes.add(key.getClass().getName());
+
+        grid.utilityCache().put(key, "test");
+
+        assertEquals("test", grid.utilityCache().get(key));
     }
 
     /** */
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeGridMonitorTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeGridMonitorTest.java
new file mode 100644
index 0000000..44a06a2
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeGridMonitorTest.java
@@ -0,0 +1,381 @@
+/*
+ * 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.compute;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.compute.ComputeJob;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.compute.ComputeTaskFuture;
+import org.apache.ignite.compute.ComputeTaskSession;
+import org.apache.ignite.compute.ComputeTaskSessionFullSupport;
+import org.apache.ignite.failure.FailureHandler;
+import org.apache.ignite.failure.StopNodeFailureHandler;
+import org.apache.ignite.internal.GridTaskSessionImpl;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.processors.task.monitor.ComputeGridMonitor;
+import org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum;
+import org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusSnapshot;
+import org.apache.ignite.internal.util.future.GridFutureAdapter;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.FAILED;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.FINISHED;
+import static org.apache.ignite.internal.processors.task.monitor.ComputeTaskStatusEnum.RUNNING;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+
+/**
+ * Test class for {@link ComputeGridMonitor}.
+ */
+public class ComputeGridMonitorTest extends GridCommonAbstractTest {
+    /** Coordinator. */
+    private static IgniteEx CRD;
+
+    /** Compute task status monitor. */
+    private ComputeGridMonitorImpl monitor;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        stopAllGrids();
+
+        IgniteEx crd = startGrids(2);
+
+        crd.cluster().state(ACTIVE);
+
+        awaitPartitionMapExchange();
+
+        CRD = crd;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        super.afterTestsStopped();
+
+        stopAllGrids();
+
+        CRD = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        CRD.context().task().listenStatusUpdates(monitor = new ComputeGridMonitorImpl());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        CRD.context().task().stopListenStatusUpdates(monitor);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected FailureHandler getFailureHandler(String igniteInstanceName) {
+        return new StopNodeFailureHandler();
+    }
+
+    /**
+     * Checking get of diffs for the successful execution of the task.
+     */
+    @Test
+    public void simpleTest() {
+        ComputeTaskFuture<Void> taskFut = CRD.compute().executeAsync(new NoopComputeTask(), null);
+
+        taskFut.get(getTestTimeout());
+
+        assertTrue(monitor.statusSnapshots.isEmpty());
+
+        assertEquals(3, monitor.statusChanges.size());
+
+        checkTaskStarted(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskMapped(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskFinished(monitor.statusChanges.poll(), taskFut.getTaskSession());
+    }
+
+    /**
+     * Checking get of diffs for the failed execution of the task.
+     */
+    @Test
+    public void failTaskTest() {
+        NoopComputeTask task = new NoopComputeTask() {
+            /**
+             * {@inheritDoc}
+             */
+            @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+                throw new IgniteException("FAIL TASK");
+            }
+        };
+
+        ComputeTaskFuture<Void> taskFut = CRD.compute().executeAsync(task, null);
+
+        assertThrows(log, () -> taskFut.get(getTestTimeout()), IgniteException.class, null);
+
+        assertTrue(monitor.statusSnapshots.isEmpty());
+
+        assertEquals(3, monitor.statusChanges.size());
+
+        checkTaskStarted(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskMapped(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskFailed(monitor.statusChanges.poll(), taskFut.getTaskSession());
+    }
+
+    /**
+     * Checking get of diffs when changing the task attribute.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void changeAttributesTest() throws Exception {
+        ComputeFullWithWaitTask task = new ComputeFullWithWaitTask(getTestTimeout());
+
+        ComputeTaskFuture<Void> taskFut = CRD.compute().executeAsync(task, null);
+
+        task.doneOnMapFut.get(getTestTimeout());
+
+        taskFut.getTaskSession().setAttribute("test", "test");
+
+        assertEquals(
+            "test",
+            taskFut.getTaskSession().waitForAttribute("test", getTestTimeout())
+        );
+
+        taskFut.get(getTestTimeout());
+
+        assertTrue(monitor.statusSnapshots.isEmpty());
+
+        assertEquals(4, monitor.statusChanges.size());
+
+        checkTaskStarted(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskMapped(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkAttributeChanged(monitor.statusChanges.poll(), taskFut.getTaskSession());
+        checkTaskFinished(monitor.statusChanges.poll(), taskFut.getTaskSession());
+    }
+
+    /**
+     * Checking the get of snapshots of task statuses.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void snapshotsTest() throws Exception {
+        ComputeFullWithWaitTask task = new ComputeFullWithWaitTask(getTestTimeout());
+
+        ComputeTaskFuture<Void> taskFut = CRD.compute().executeAsync(task, null);
+
+        task.doneOnMapFut.get(getTestTimeout());
+
+        ComputeGridMonitorImpl monitor1 = new ComputeGridMonitorImpl();
+
+        try {
+            CRD.context().task().listenStatusUpdates(monitor1);
+
+            assertTrue(monitor.statusSnapshots.isEmpty());
+
+            assertEquals(1, monitor1.statusSnapshots.size());
+
+            checkSnapshot(monitor1.statusSnapshots.poll(), taskFut.getTaskSession());
+        }
+        finally {
+            CRD.context().task().stopListenStatusUpdates(monitor1);
+        }
+
+        taskFut.get(getTestTimeout());
+    }
+
+    /** */
+    private void checkTaskStarted(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, RUNNING, false, false);
+    }
+
+    /** */
+    private void checkTaskMapped(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, RUNNING, true, false);
+    }
+
+    /** */
+    private void checkAttributeChanged(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, RUNNING, true, true);
+    }
+
+    /** */
+    private void checkTaskFinished(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, FINISHED, true, true);
+    }
+
+    /** */
+    private void checkTaskFailed(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, FAILED, true, true);
+    }
+
+    /** */
+    private void checkSnapshot(ComputeTaskStatusSnapshot snapshot, ComputeTaskSession session) {
+        checkSnapshot(snapshot, (GridTaskSessionImpl)session, RUNNING, true, true);
+    }
+
+    /** */
+    private void checkSnapshot(
+        ComputeTaskStatusSnapshot snapshot,
+        GridTaskSessionImpl session,
+        ComputeTaskStatusEnum expStatus,
+        boolean checkJobNodes,
+        boolean checkAttributes
+    ) {
+        assertEquals(session.getId(), snapshot.sessionId());
+        assertEquals(expStatus, snapshot.status());
+
+        assertEquals(session.getTaskName(), snapshot.taskName());
+        assertEquals(session.getTaskNodeId(), snapshot.originatingNodeId());
+        assertEquals(session.getStartTime(), snapshot.startTime());
+        assertEquals(session.isFullSupport(), snapshot.fullSupport());
+        assertEquals(session.isInternal(), session.isInternal());
+
+        checkLogin(session, snapshot);
+
+        if (checkJobNodes) {
+            assertEquals(
+                new TreeSet<>(session.getTopology()),
+                new TreeSet<>(snapshot.jobNodes())
+            );
+        }
+        else
+            assertTrue(snapshot.jobNodes().isEmpty());
+
+        if (checkAttributes && session.isFullSupport()) {
+            assertEquals(
+                new TreeMap<>(session.getAttributes()),
+                new TreeMap<>(snapshot.attributes())
+            );
+        }
+
+        if (expStatus == FINISHED) {
+            assertTrue(snapshot.endTime() > 0L);
+            assertNull(snapshot.failReason());
+        }
+        else if (expStatus == FAILED) {
+            assertTrue(snapshot.endTime() > 0L);
+            assertNotNull(snapshot.failReason());
+        }
+        else {
+            assertEquals(0L, snapshot.endTime());
+            assertNull(snapshot.failReason());
+        }
+    }
+
+    /** */
+    private static class ComputeGridMonitorImpl implements ComputeGridMonitor {
+        /** */
+        final Queue<ComputeTaskStatusSnapshot> statusSnapshots = new ConcurrentLinkedQueue<>();
+
+        /** */
+        final Queue<ComputeTaskStatusSnapshot> statusChanges = new ConcurrentLinkedQueue<>();
+
+        /** {@inheritDoc} */
+        @Override public void processStatusSnapshots(Collection<ComputeTaskStatusSnapshot> snapshots) {
+            statusSnapshots.addAll(snapshots);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void processStatusChange(ComputeTaskStatusSnapshot snapshot) {
+            statusChanges.add(snapshot);
+        }
+    }
+
+    /** */
+    private static class NoopComputeTask extends ComputeTaskAdapter<Void, Void> {
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            return subgrid.stream().collect(toMap(n -> new NoopJob(), identity()));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+    }
+
+    /** */
+    @ComputeTaskSessionFullSupport
+    private static class ComputeFullWithWaitTask extends ComputeTaskAdapter<Void, Void> {
+        /** */
+        final GridFutureAdapter<Void> doneOnMapFut = new GridFutureAdapter<>();
+
+        /** */
+        final long timeout;
+
+        /** */
+        public ComputeFullWithWaitTask(long timeout) {
+            this.timeout = timeout;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            doneOnMapFut.onDone();
+
+            return subgrid.stream().collect(toMap(n -> new NoopJob() {
+                /** {@inheritDoc} */
+                @Override public Object execute() throws IgniteException {
+                    try {
+                        U.sleep(500);
+                    }
+                    catch (IgniteInterruptedCheckedException e) {
+                        throw new IgniteException(e);
+                    }
+
+                    return super.execute();
+                }
+            }, identity()));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+    }
+
+    /**
+     * @param session Task session.
+     * @param snapshot Task status snapshot.
+     */
+    protected void checkLogin(GridTaskSessionImpl session, ComputeTaskStatusSnapshot snapshot) {
+        assertNull(session.login());
+        assertNull(snapshot.createBy());
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobChangePriorityTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobChangePriorityTest.java
new file mode 100644
index 0000000..8727164
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobChangePriorityTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.compute;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Stream;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.compute.ComputeJob;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.compute.ComputeTaskFuture;
+import org.apache.ignite.compute.ComputeTaskSessionFullSupport;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.failure.StopNodeFailureHandler;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
+import org.apache.ignite.internal.processors.job.GridJobProcessor;
+import org.apache.ignite.internal.util.future.GridFutureAdapter;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.spi.collision.CollisionContext;
+import org.apache.ignite.spi.collision.CollisionJobContext;
+import org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+
+/**
+ * Class for testing job priority change.
+ */
+public class ComputeJobChangePriorityTest extends GridCommonAbstractTest {
+    /** Coordinator. */
+    private static IgniteEx CRD;
+
+    /** */
+    private static Method ON_CHANGE_TASK_ATTRS_MTD;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        stopAllGrids();
+
+        IgniteEx crd = startGrids(2);
+
+        crd.cluster().state(ACTIVE);
+
+        awaitPartitionMapExchange();
+
+        CRD = crd;
+
+        ON_CHANGE_TASK_ATTRS_MTD = GridJobProcessor.class.getDeclaredMethod(
+            "onChangeTaskAttributes",
+            IgniteUuid.class,
+            IgniteUuid.class,
+            Map.class
+        );
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        super.afterTestsStopped();
+
+        stopAllGrids();
+
+        CRD = null;
+        ON_CHANGE_TASK_ATTRS_MTD = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        for (Ignite n : G.allGrids())
+            PriorityQueueCollisionSpiEx.spiEx(n).reset();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setFailureHandler(new StopNodeFailureHandler())
+            .setCollisionSpi(new PriorityQueueCollisionSpiEx())
+            .setMetricsUpdateFrequency(Long.MAX_VALUE)
+            .setClientFailureDetectionTimeout(Long.MAX_VALUE);
+    }
+
+    /**
+     * Checking that when {@link PriorityQueueCollisionSpi#getPriorityAttributeKey} is changed,
+     * collisions will be handled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testChangeTaskPriorityAttribute() throws Exception {
+        checkChangeAttributes(
+            PriorityQueueCollisionSpiEx.spiEx(CRD).getPriorityAttributeKey(),
+            1,
+            true
+        );
+    }
+
+    /**
+     * Checking that when {@link PriorityQueueCollisionSpi#getJobPriorityAttributeKey} is changed,
+     * collisions will be handled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testChangeJobPriorityAttribute() throws Exception {
+        checkChangeAttributes(
+            PriorityQueueCollisionSpiEx.spiEx(CRD).getJobPriorityAttributeKey(),
+            1,
+            true
+        );
+    }
+
+    /**
+     * Checking that no collision handling will occur when a random attribute is changed.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testChangeRandomAttribute() throws Exception {
+        checkChangeAttributes(
+            UUID.randomUUID().toString(),
+            UUID.randomUUID().toString(),
+            false
+        );
+    }
+
+    /** */
+    private void checkChangeAttributes(
+        String key,
+        Object val,
+        boolean expHandleCollisionOnChangeTaskAttrs
+    ) throws Exception {
+        ComputeTaskFuture<Void> taskFut = CRD.compute().executeAsync(new NoopTask(), null);
+
+        for (Ignite n : G.allGrids())
+            PriorityQueueCollisionSpiEx.spiEx(n).waitJobFut.get(getTestTimeout());
+
+        for (Ignite n : G.allGrids())
+            PriorityQueueCollisionSpiEx.spiEx(n).handleCollision = true;
+
+        taskFut.getTaskSession().setAttribute(key, val);
+
+        for (Ignite n : G.allGrids()) {
+            assertEquals(
+                val,
+                PriorityQueueCollisionSpiEx.spiEx(n).waitJobFut.result()
+                    .getTaskSession().waitForAttribute(key, getTestTimeout()));
+        }
+
+        for (Ignite n : G.allGrids()) {
+            GridFutureAdapter<Void> fut = PriorityQueueCollisionSpiEx.spiEx(n).onChangeTaskAttrsFut;
+
+            if (expHandleCollisionOnChangeTaskAttrs)
+                fut.get(getTestTimeout());
+            else
+                assertThrows(log, () -> fut.get(100), IgniteFutureTimeoutCheckedException.class, null);
+        }
+
+        if (!expHandleCollisionOnChangeTaskAttrs)
+            CRD.compute().execute(new NoopTask(), null);
+
+        taskFut.get(getTestTimeout());
+    }
+
+    /** */
+    private static class PriorityQueueCollisionSpiEx extends PriorityQueueCollisionSpi {
+        /** */
+        volatile boolean handleCollision;
+
+        /** */
+        final GridFutureAdapter<CollisionJobContext> waitJobFut = new GridFutureAdapter<>();
+
+        /** */
+        final GridFutureAdapter<Void> onChangeTaskAttrsFut = new GridFutureAdapter<>();
+
+        /** {@inheritDoc} */
+        @Override public void onCollision(CollisionContext ctx) {
+            if (!waitJobFut.isDone()) {
+                ctx.waitingJobs().stream()
+                    .filter(collisionJobCtx -> collisionJobCtx.getJob() instanceof NoopJob)
+                    .findAny()
+                    .ifPresent(waitJobFut::onDone);
+            }
+
+            if (handleCollision) {
+                if (!onChangeTaskAttrsFut.isDone()) {
+                    Stream.of(new Exception().getStackTrace())
+                        .filter(el ->
+                            ON_CHANGE_TASK_ATTRS_MTD.getDeclaringClass().getName().equals(el.getClassName()) &&
+                                ON_CHANGE_TASK_ATTRS_MTD.getName().equals(el.getMethodName())
+                        )
+                        .findAny()
+                        .ifPresent(el -> onChangeTaskAttrsFut.onDone());
+                }
+
+                super.onCollision(ctx);
+            }
+        }
+
+        /** */
+        void reset() {
+            handleCollision = false;
+
+            waitJobFut.reset();
+
+            onChangeTaskAttrsFut.reset();
+        }
+
+        /** */
+        static PriorityQueueCollisionSpiEx spiEx(Ignite n) {
+            return ((PriorityQueueCollisionSpiEx)((IgniteEx)n).context().config().getCollisionSpi());
+        }
+    }
+
+    /** */
+    @ComputeTaskSessionFullSupport
+    private static class NoopTask extends ComputeTaskAdapter<Void, Void> {
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            return subgrid.stream().collect(toMap(n -> new NoopJob(), identity()));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobStatusTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobStatusTest.java
new file mode 100644
index 0000000..17c783d
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeJobStatusTest.java
@@ -0,0 +1,457 @@
+/*
+ * 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.compute;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+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.ComputeJobResultPolicy;
+import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.compute.ComputeTaskFuture;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.failure.StopNodeFailureHandler;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.managers.collision.GridCollisionManager;
+import org.apache.ignite.internal.processors.job.ComputeJobStatusEnum;
+import org.apache.ignite.internal.processors.job.GridJobProcessor;
+import org.apache.ignite.internal.processors.task.GridTaskProcessor;
+import org.apache.ignite.internal.util.future.GridFutureAdapter;
+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.lang.IgniteUuid;
+import org.apache.ignite.spi.collision.CollisionContext;
+import org.apache.ignite.spi.collision.CollisionExternalListener;
+import org.apache.ignite.spi.collision.CollisionJobContext;
+import org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+import static java.util.Collections.emptyMap;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.CANCELLED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FAILED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.FINISHED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.QUEUED;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.RUNNING;
+import static org.apache.ignite.internal.processors.job.ComputeJobStatusEnum.SUSPENDED;
+import static org.apache.ignite.testframework.GridTestUtils.getFieldValue;
+
+/**
+ * Class for testing {@link GridTaskProcessor#jobStatuses} and {@link GridJobProcessor#jobStatuses}.
+ */
+public class ComputeJobStatusTest extends GridCommonAbstractTest {
+    /** Coordinator. */
+    private static IgniteEx node0;
+
+    /** Second node. */
+    private static IgniteEx node1;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        stopAllGrids();
+
+        IgniteEx crd = startGrids(2);
+
+        crd.cluster().state(ACTIVE);
+
+        awaitPartitionMapExchange();
+
+        node0 = crd;
+        node1 = grid(1);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        super.afterTestsStopped();
+
+        stopAllGrids();
+
+        node0 = null;
+        node1 = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        applyAllNodes(PriorityQueueCollisionSpiEx::reset);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setFailureHandler(new StopNodeFailureHandler())
+            .setCollisionSpi(new PriorityQueueCollisionSpiEx())
+            // Disable automatic update of metrics, which can call PriorityQueueCollisionSpi.onCollision
+            // - leads to the activation (execute) of jobs.
+            .setMetricsUpdateFrequency(Long.MAX_VALUE)
+            .setClientFailureDetectionTimeout(Long.MAX_VALUE);
+    }
+
+    /**
+     * Check that there will be no errors if they request statuses for non-existing tasks.
+     */
+    @Test
+    public void testNoStatistics() {
+        IgniteUuid sesId = IgniteUuid.fromUuid(UUID.randomUUID());
+
+        checkTaskJobStatuses(sesId, null, null);
+        checkJobJobStatuses(sesId, null, null);
+    }
+
+    /**
+     * Checks that the statuses of the job will be:
+     * {@link ComputeJobStatusEnum#QUEUED} -> {@link ComputeJobStatusEnum#RUNNING} -> {@link ComputeJobStatusEnum#FINISHED}.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testFinishedTasks() throws Exception {
+        checkJobStatuses(FINISHED);
+    }
+
+    /**
+     * Checks that the statuses of the work will be:
+     * {@link ComputeJobStatusEnum#QUEUED} -> {@link ComputeJobStatusEnum#RUNNING} -> {@link ComputeJobStatusEnum#FAILED}.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testFailedTasks() throws Exception {
+        checkJobStatuses(FAILED);
+    }
+
+    /**
+     * Checks that the statuses of the work will be:
+     * {@link ComputeJobStatusEnum#QUEUED} -> {@link ComputeJobStatusEnum#RUNNING} -> {@link ComputeJobStatusEnum#CANCELLED}.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void tesCancelledTasks() throws Exception {
+        checkJobStatuses(CANCELLED);
+    }
+
+    /**
+     * Checks that the statuses of the work will be:
+     * {@link ComputeJobStatusEnum#QUEUED} -> {@link ComputeJobStatusEnum#RUNNING} ->
+     * {@link ComputeJobStatusEnum#SUSPENDED} -> {@link ComputeJobStatusEnum#FINISHED}.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void tesSuspendedTasks() throws Exception {
+        checkJobStatuses(SUSPENDED);
+    }
+
+    /** */
+    private void checkJobStatuses(ComputeJobStatusEnum exp) throws Exception {
+        applyAllNodes(spiEx -> spiEx.waitJobCls = WaitJob.class);
+
+        ComputeTaskFuture<Void> taskFut = node0.compute().executeAsync(
+            new SimpleTask(() -> new WaitJob(getTestTimeout())),
+            null
+        );
+
+        // We are waiting for the jobs (PriorityQueueCollisionSpiEx#waitJobCls == WaitJob.class)
+        // to be received on the nodes to ensure that the correct statistics are obtained.
+        applyAllNodes(spiEx -> spiEx.waitJobFut.get(getTestTimeout()));
+
+        IgniteUuid sesId = taskFut.getTaskSession().getId();
+
+        checkTaskJobStatuses(sesId, QUEUED, null);
+        checkJobJobStatuses(sesId, QUEUED, QUEUED);
+
+        PriorityQueueCollisionSpiEx spiEx0 = PriorityQueueCollisionSpiEx.spiEx(node0);
+        PriorityQueueCollisionSpiEx spiEx1 = PriorityQueueCollisionSpiEx.spiEx(node1);
+
+        WaitJob waitJob0 = spiEx0.task();
+        WaitJob waitJob1 = spiEx1.task();
+
+        // Activating and waiting for a job (WaitJob) to start on node0.
+        spiEx0.handleCollisions();
+        waitJob0.onStartFut.get(getTestTimeout());
+
+        checkTaskJobStatuses(sesId, RUNNING, null);
+        checkJobJobStatuses(sesId, RUNNING, QUEUED);
+
+        // Activating and waiting for a job (WaitJob) to start on node1.
+        spiEx1.handleCollisions();
+        waitJob1.onStartFut.get(getTestTimeout());
+
+        checkTaskJobStatuses(sesId, RUNNING, null);
+        checkJobJobStatuses(sesId, RUNNING, RUNNING);
+
+        switch (exp) {
+            case FINISHED:
+                // Just letting job (WaitJob) finish on node0.
+                waitJob0.waitFut.onDone();
+                break;
+
+            case FAILED:
+                // Finish the job (WaitJob) with an error on node0.
+                waitJob0.waitFut.onDone(new Exception("from test"));
+                break;
+
+            case CANCELLED:
+                // Cancel the job (WaitJob) on node0.
+                node0.context().job().cancelJob(
+                    sesId,
+                    spiEx0.waitJobFut.result().getJobContext().getJobId(),
+                    false
+                );
+                break;
+
+            case SUSPENDED:
+                // Hold the job (WaitJob) with on node0.
+                spiEx0.waitJobFut.result().getJobContext().holdcc();
+                break;
+
+            default:
+                fail("Unknown: " + exp);
+        }
+
+        // Let's wait a bit for the operation (above) to complete.
+        U.sleep(100);
+
+        checkTaskJobStatuses(sesId, exp, null);
+
+        if (exp == SUSPENDED) {
+            // Must resume (unhold) the job (WaitJob) to finish correctly.
+            checkJobJobStatuses(sesId, exp, RUNNING);
+            waitJob0.waitFut.onDone();
+            spiEx0.waitJobFut.result().getJobContext().callcc();
+
+            U.sleep(100);
+
+            checkTaskJobStatuses(sesId, FINISHED, null);
+        }
+
+        // Let's check that the job (WaitJob) on the node0 has finished
+        // and that the statistics about it will be empty (on node0).
+        checkJobJobStatuses(sesId, null, RUNNING);
+
+        // Let's finish the job (WaitJob) on node1.
+        waitJob1.waitFut.onDone();
+
+        taskFut.get(getTestTimeout());
+
+        // After the completion of the task, we will no longer receive statistics about it.
+        checkTaskJobStatuses(sesId, null, null);
+        checkJobJobStatuses(sesId, null, null);
+    }
+
+    /** */
+    private void checkTaskJobStatuses(
+        IgniteUuid sesId,
+        @Nullable ComputeJobStatusEnum expN0,
+        @Nullable ComputeJobStatusEnum expN1
+    ) {
+        Map<ComputeJobStatusEnum, Long> exp0 = expN0 == null ? emptyMap() : F.asMap(expN0, 1L);
+        Map<ComputeJobStatusEnum, Long> exp1 = expN1 == null ? emptyMap() : F.asMap(expN1, 1L);
+
+        assertEqualsMaps(exp0, node0.context().task().jobStatuses(sesId));
+        assertEqualsMaps(exp1, node1.context().task().jobStatuses(sesId));
+    }
+
+    /** */
+    private void checkJobJobStatuses(
+        IgniteUuid sesId,
+        @Nullable ComputeJobStatusEnum expN0,
+        @Nullable ComputeJobStatusEnum expN1
+    ) {
+        Map<ComputeJobStatusEnum, Long> exp0 = expN0 == null ? emptyMap() : F.asMap(expN0, 1L);
+        Map<ComputeJobStatusEnum, Long> exp1 = expN1 == null ? emptyMap() : F.asMap(expN1, 1L);
+
+        assertEqualsMaps(exp0, node0.context().job().jobStatuses(sesId));
+        assertEqualsMaps(exp1, node1.context().job().jobStatuses(sesId));
+    }
+
+    /** */
+    private void applyAllNodes(ConsumerX<PriorityQueueCollisionSpiEx> c) throws Exception {
+        for (Ignite n : G.allGrids())
+            c.accept(PriorityQueueCollisionSpiEx.spiEx(n));
+    }
+
+    /** */
+    private static class PriorityQueueCollisionSpiEx extends PriorityQueueCollisionSpi {
+        /** */
+        volatile boolean handleCollision;
+
+        /** */
+        @Nullable volatile Class<? extends ComputeJob> waitJobCls;
+
+        /** */
+        final GridFutureAdapter<CollisionJobContext> waitJobFut = new GridFutureAdapter<>();
+
+        /** {@inheritDoc} */
+        @Override public void onCollision(CollisionContext ctx) {
+            if (!waitJobFut.isDone()) {
+                Class<? extends ComputeJob> waitJobCls = this.waitJobCls;
+
+                if (waitJobCls != null)
+                    ctx.waitingJobs().stream()
+                        .filter(jobCtx -> waitJobCls.isInstance(jobCtx.getJob()))
+                        .findAny()
+                        .ifPresent(waitJobFut::onDone);
+            }
+
+            if (handleCollision)
+                super.onCollision(ctx);
+        }
+
+        /** */
+        void reset() {
+            handleCollision = false;
+
+            waitJobCls = null;
+
+            waitJobFut.reset();
+        }
+
+        /** */
+        void handleCollisions() {
+            handleCollision = true;
+
+            GridCollisionManager collision = ((IgniteEx)ignite).context().collision();
+
+            AtomicReference<CollisionExternalListener> extLsnr = getFieldValue(collision, "extLsnr");
+
+            CollisionExternalListener lsnr = extLsnr.get();
+
+            assertNotNull(lsnr);
+
+            lsnr.onExternalCollision();
+        }
+
+        /** */
+        <T> T task() {
+            return (T)waitJobFut.result().getJob();
+        }
+
+        /** */
+        static PriorityQueueCollisionSpiEx spiEx(Ignite n) {
+            return ((PriorityQueueCollisionSpiEx)n.configuration().getCollisionSpi());
+        }
+    }
+
+    /** */
+    private interface ConsumerX<T> {
+        /** */
+        void accept(T t) throws Exception;
+    }
+
+    /** */
+    private static class SimpleTask extends ComputeTaskAdapter<Void, Void> {
+        /** */
+        final Supplier<? extends ComputeJob> jobFactory;
+
+        /** */
+        private SimpleTask(Supplier<? extends ComputeJob> factory) {
+            jobFactory = factory;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            return subgrid.stream().collect(toMap(n -> jobFactory.get(), identity()));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public ComputeJobResultPolicy result(
+            ComputeJobResult res,
+            List<ComputeJobResult> rcvd
+        ) throws IgniteException {
+            return ComputeJobResultPolicy.WAIT;
+        }
+    }
+
+    /** */
+    private static class WaitJob extends ComputeJobAdapter implements Externalizable {
+        /** */
+        GridFutureAdapter<Void> onStartFut;
+
+        /** */
+        GridFutureAdapter<Void> waitFut;
+
+        /** */
+        long waitTimeout;
+
+        /** */
+        public WaitJob() {
+            onStartFut = new GridFutureAdapter<>();
+            waitFut = new GridFutureAdapter<>();
+        }
+
+        /** */
+        private WaitJob(long timeout) {
+            this();
+
+            waitTimeout = timeout;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Object execute() throws IgniteException {
+            onStartFut.onDone();
+
+            try {
+                waitFut.get(waitTimeout);
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException(e);
+            }
+
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void writeExternal(ObjectOutput out) throws IOException {
+            out.writeLong(waitTimeout);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+            waitTimeout = in.readLong();
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeTaskWithWithoutFullSupportTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeTaskWithWithoutFullSupportTest.java
new file mode 100644
index 0000000..cc39cea
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/ComputeTaskWithWithoutFullSupportTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.compute;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.compute.ComputeJob;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.compute.ComputeTaskFuture;
+import org.apache.ignite.compute.ComputeTaskSessionFullSupport;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.failure.StopNodeFailureHandler;
+import org.apache.ignite.internal.GridTaskSessionInternal;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.spi.collision.CollisionContext;
+import org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static java.util.Collections.singletonMap;
+import static org.apache.ignite.cluster.ClusterState.ACTIVE;
+
+/**
+ * Class for checking that there will be no errors when starting tasks with/without
+ * {@link ComputeTaskSessionFullSupport}.
+ */
+public class ComputeTaskWithWithoutFullSupportTest extends GridCommonAbstractTest {
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        stopAllGrids();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        stopAllGrids();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setCollisionSpi(new PriorityQueueCollisionSpiEx().setParallelJobsNumber(1))
+            .setFailureHandler(new StopNodeFailureHandler())
+            .setMetricsUpdateFrequency(Long.MAX_VALUE)
+            .setClientFailureDetectionTimeout(Long.MAX_VALUE);
+    }
+
+    /**
+     * Checking that if there is {@link PriorityQueueCollisionSpi},
+     * it is possible to run tasks with and without {@link ComputeTaskSessionFullSupport}.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void test() throws Exception {
+        IgniteEx n = startGrid(0);
+
+        n.cluster().state(ACTIVE);
+
+        ComputeTaskFuture<Void> taskFut0 = n.compute().executeAsync(new TaskWithFullSupport(), null);
+        assertTrue(((GridTaskSessionInternal)taskFut0.getTaskSession()).isFullSupport());
+
+        ((PriorityQueueCollisionSpiEx)n.configuration().getCollisionSpi()).handleCollision = true;
+
+        ComputeTaskFuture<Void> taskFut1 = n.compute().executeAsync(new TaskWithoutFullSupport(), null);
+        assertFalse(((GridTaskSessionInternal)taskFut1.getTaskSession()).isFullSupport());
+
+        taskFut0.get(TimeUnit.SECONDS.toMillis(1));
+        taskFut1.get(TimeUnit.SECONDS.toMillis(1));
+    }
+
+    /** */
+    private static class PriorityQueueCollisionSpiEx extends PriorityQueueCollisionSpi {
+        /** */
+        volatile boolean handleCollision;
+
+        /** {@inheritDoc} */
+        @Override public void onCollision(CollisionContext ctx) {
+            if (handleCollision)
+                super.onCollision(ctx);
+        }
+    }
+
+    /** */
+    @ComputeTaskSessionFullSupport
+    private static class TaskWithFullSupport extends ComputeTaskAdapter<Void, Void> {
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            assertFalse(subgrid.isEmpty());
+
+            return singletonMap(new NoopJob(), subgrid.get(0));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+    }
+
+    /** */
+    private static class TaskWithoutFullSupport extends ComputeTaskAdapter<Void, Void> {
+        /** {@inheritDoc} */
+        @Override public Map<? extends ComputeJob, ClusterNode> map(
+            List<ClusterNode> subgrid,
+            Void arg
+        ) throws IgniteException {
+            assertFalse(subgrid.isEmpty());
+
+            return singletonMap(new NoopJob(), subgrid.get(0));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+            return null;
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/NoopJob.java
similarity index 74%
copy from modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java
copy to modules/core/src/test/java/org/apache/ignite/internal/processors/compute/NoopJob.java
index 13ed4bd..48f8bc0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/consistency/SingleBackupExplicitTransactionalReadRepairTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/compute/NoopJob.java
@@ -15,14 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.processors.cache.consistency;
+package org.apache.ignite.internal.processors.compute;
+
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobAdapter;
 
 /**
  *
  */
-public class SingleBackupExplicitTransactionalReadRepairTest extends ExplicitTransactionalReadRepairTest {
+class NoopJob extends ComputeJobAdapter {
     /** {@inheritDoc} */
-    @Override protected Integer backupsCount() {
-        return 1; // Single backup possible optimisations check.
+    @Override public Object execute() throws IgniteException {
+        return null;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReplaceRemoveRaceTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReplaceRemoveRaceTest.java
index ecfc093..8a4c59c 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReplaceRemoveRaceTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReplaceRemoveRaceTest.java
@@ -18,12 +18,10 @@
 package org.apache.ignite.internal.processors.database;
 
 import java.io.Externalizable;
-import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.IgniteCheckedException;
-import org.apache.ignite.IgniteException;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.failure.FailureContext;
 import org.apache.ignite.internal.IgniteInternalFuture;
@@ -41,15 +39,14 @@
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusLeafIO;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.IOVersions;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
-import org.apache.ignite.internal.processors.cache.persistence.tree.reuse.ReuseList;
 import org.apache.ignite.internal.processors.failure.FailureProcessor;
 import org.apache.ignite.internal.util.typedef.T2;
-import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.GridTestKernalContext;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 import static org.apache.ignite.internal.util.IgniteUtils.MB;
+import static org.apache.ignite.testframework.GridTestUtils.runAsync;
 
 /**
  * Test is based on {@link BPlusTreeSelfTest} and has a partial copy of its code.
@@ -147,16 +144,14 @@
      */
     protected static class TestPairTree extends BPlusTree<Pair, Pair> {
         /**
-         * @param reuseList Reuse list.
-         * @param canGetRow Can get row from inner page.
+         * Constructor.
+         *
          * @param cacheId Cache ID.
          * @param pageMem Page memory.
          * @param metaPageId Meta page ID.
          * @throws IgniteCheckedException If failed.
          */
         public TestPairTree(
-            ReuseList reuseList,
-            boolean canGetRow,
             int cacheId,
             PageMemory pageMem,
             long metaPageId,
@@ -170,7 +165,7 @@
                 null,
                 new AtomicLong(),
                 metaPageId,
-                reuseList,
+                null,
                 new IOVersions<>(new TestPairInnerIO()),
                 new IOVersions<>(new TestPairLeafIO()),
                 PageIdAllocator.FLAG_IDX,
@@ -347,60 +342,31 @@
      *
      * Several iterations are required for this, given that there's no guaranteed way to force a tree to perform page
      * modifications in the desired order. Typically, less than {@code 10} attempts have been required to get a
-     * corrupted tree. Value {@code 50} is arbitrary and has been chosen to be big enough for test to fail in case of
+     * corrupted tree. Value {@code 100} is arbitrary and has been chosen to be big enough for test to fail in case of
      * regression, but not too big so that test won't run for too long.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testConcurrentPutRemove() throws Exception {
-        for (int i = 0; i < 50; i++) {
-            TestPairTree tree = new TestPairTree(
-                null,
-                true,
-                CACHE_ID,
-                pageMem,
-                allocateMetaPage().pageId(),
-                lockTrackerManager
-            );
-
-            tree.putx(new Pair(1, 0));
-            tree.putx(new Pair(2, 0));
-            tree.putx(new Pair(4, 0));
-            tree.putx(new Pair(6, 0));
-            tree.putx(new Pair(7, 0));
-
-            // Split root.
-            tree.putx(new Pair(5, 0));
-
-            // Split its left subtree.
-            tree.putx(new Pair(3, 0));
+        for (int i = 0; i < 100; i++) {
+            TestPairTree tree = prepareBPlusTree();
 
             // Exact tree from the description is constructed at this point.
             CyclicBarrier barrier = new CyclicBarrier(2);
 
             // This is the replace operation.
-            IgniteInternalFuture<?> putFut = GridTestUtils.runAsync(() -> {
-                try {
-                    barrier.await();
+            IgniteInternalFuture<?> putFut = runAsync(() -> {
+                barrier.await();
 
-                    tree.putx(new Pair(4, 999));
-                }
-                catch (IgniteCheckedException | BrokenBarrierException | InterruptedException e) {
-                    throw new IgniteException(e);
-                }
+                tree.putx(new Pair(4, 999));
             });
 
-            // This is the remove opertation.
-            IgniteInternalFuture<?> remFut = GridTestUtils.runAsync(() -> {
-                try {
-                    barrier.await();
+            // This is the remove operation.
+            IgniteInternalFuture<?> remFut = runAsync(() -> {
+                barrier.await();
 
-                    tree.removex(new Pair(5, -1));
-                }
-                catch (IgniteCheckedException | BrokenBarrierException | InterruptedException e) {
-                    throw new IgniteException(e);
-                }
+                tree.removex(new Pair(5, -1));
             });
 
             // Wait for both operations.
@@ -421,4 +387,84 @@
             assertEquals(999, pair.getValue().intValue());
         }
     }
+
+    /**
+     * Checks that there will be no corrupted B+tree during concurrent update and deletion
+     * of the same key that is contained in the inner and leaf nodes of the B+tree.
+     *
+     * NOTE: Test logic is the same as of {@link #testConcurrentPutRemove},
+     * the only difference is that it operates (puts and removes) on a single key.
+     * 
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testConcurrentPutRemoveSameRow() throws Exception {
+        for (int i = 0; i < 100; i++) {
+            TestPairTree tree = prepareBPlusTree();
+
+            // Exact tree from the description is constructed at this point.
+            CyclicBarrier barrier = new CyclicBarrier(2);
+
+            // This is the replace operation.
+            IgniteInternalFuture<?> putFut = runAsync(() -> {
+                barrier.await();
+
+                tree.putx(new Pair(5, 999));
+            });
+
+            // This is the remove operation.
+            IgniteInternalFuture<?> remFut = runAsync(() -> {
+                barrier.await();
+
+                tree.removex(new Pair(5, 0));
+            });
+
+            // Wait for both operations.
+            try {
+                putFut.get(1, TimeUnit.SECONDS);
+            }
+            finally {
+                remFut.get(1, TimeUnit.SECONDS);
+            }
+
+            // Just in case.
+            tree.validateTree();
+        }
+    }
+
+    /**
+     * Creates and fills a tree:
+     * <pre><code>
+     *                                    [ 5:0 ]
+     *                                /            \
+     *                 [ 2:0 | 4:0 ]                  [ 6:0 ]
+     *               /       |       \              /      |
+     * [ 1:0 | 2:0 ]->[ 3:0 | 4:0 ]->[ 5:0 ]->[ 6:0 ]->[ 7:0 ]
+     * </code></pre>
+     *
+     * @return New B+tree.
+     * @throws Exception If failed.
+     */
+    private TestPairTree prepareBPlusTree() throws Exception {
+        TestPairTree tree = new TestPairTree(
+            CACHE_ID,
+            pageMem,
+            allocateMetaPage().pageId(),
+            lockTrackerManager
+        );
+
+        tree.putx(new Pair(1, 0));
+        tree.putx(new Pair(2, 0));
+        tree.putx(new Pair(4, 0));
+        tree.putx(new Pair(6, 0));
+        tree.putx(new Pair(7, 0));
+
+        // Split root.
+        tree.putx(new Pair(5, 0));
+
+        // Split its left subtree.
+        tree.putx(new Pair(3, 0));
+
+        return tree;
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.java
new file mode 100644
index 0000000..9b036cf
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.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.service;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import com.google.common.collect.Iterables;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.metric.GridMetricManager;
+import org.apache.ignite.internal.processors.service.inner.MyService;
+import org.apache.ignite.internal.processors.service.inner.MyServiceFactory;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.spi.metric.HistogramMetric;
+import org.apache.ignite.spi.metric.Metric;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
+import org.apache.ignite.spi.metric.jmx.JmxMetricExporterSpi;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.service.IgniteServiceProcessor.serviceMetricRegistryName;
+
+/**
+ * Tests metrics of service invocations.
+ */
+public class GridServiceMetricsTest extends GridCommonAbstractTest {
+    /** Number of service invocations. */
+    private static final int INVOKE_CNT = 50;
+
+    /** Service name used in the tests. */
+    private static final String SRVC_NAME = "TestService";
+
+    /** Error message of created metrics. */
+    private static final String METRICS_MUST_NOT_BE_CREATED = "Service metric registry must not be created.";
+
+    /** Utility holder of current grid number. */
+    private int gridNum;
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        // JMX metrics exposition to see actual namings and placement of the metrics.
+        cfg.setMetricExporterSpi(new JmxMetricExporterSpi());
+
+        return cfg;
+    }
+
+    /** Checks service metrics are enabled / disabled properly. */
+    @Test
+    public void testServiceMetricsEnabledDisabled() throws Exception {
+        IgniteEx ignite = startGrid();
+
+        ServiceConfiguration srvcCfg = serviceCfg(MyServiceFactory.create(), 0, 1);
+
+        srvcCfg.setStatisticsEnabled(false);
+
+        ignite.services().deploy(srvcCfg);
+
+        assertNull(METRICS_MUST_NOT_BE_CREATED, findMetricRegistry(ignite.context().metric(), SRVC_NAME));
+
+        ignite.services().cancel(SRVC_NAME);
+
+        srvcCfg.setStatisticsEnabled(true);
+
+        ignite.services().deploy(srvcCfg);
+
+        assertNotNull("Service metric registry must be created.",
+            findMetricRegistry(ignite.context().metric(), SRVC_NAME));
+    }
+
+    /** Checks metric are created when service is deployed and removed when service is undeployed. */
+    @Test
+    public void testMetricsOnServiceDeployAndCancel() throws Exception {
+        List<IgniteEx> grids = startGrids(3, false);
+
+        // 2 services per node.
+        grids.get(0).services().deploy(serviceCfg(MyServiceFactory.create(), grids.size() * 2, 2));
+
+        awaitPartitionMapExchange();
+
+        int expectedCnt = Arrays.stream(MyService.class.getDeclaredMethods()).map(Method::getName).collect(
+            Collectors.toSet()).size();
+
+        // Make sure metrics are registered.
+        for (IgniteEx ignite : grids)
+            assertEquals(metricsCnt(ignite), expectedCnt);
+
+        grids.get(0).services().cancel(SRVC_NAME);
+
+        awaitPartitionMapExchange();
+
+        for (IgniteEx ignite : grids)
+            assertEquals(metricsCnt(ignite), 0);
+    }
+
+    /** Tests service metrics migrates correclty with the service redeployment. */
+    @Test
+    public void testRedeploy() throws Exception {
+        List<IgniteEx> grids = startGrids(3, false);
+
+        // 2 services per node.
+        grid(0).services().deploy(serviceCfg(MyServiceFactory.create(), 1, 0));
+
+        awaitPartitionMapExchange();
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        int expectedCnt = Arrays.stream(MyService.class.getDeclaredMethods()).map(Method::getName).collect(
+            Collectors.toSet()).size();
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        assertEquals("Only one metric registry can persist for one service instance", expectedCnt,
+            grids.stream().map(GridServiceMetricsTest::metricsCnt).mapToInt(Integer::intValue).sum());
+
+        for (int i = 0; i < grids.size(); ++i) {
+            if (metricsCnt(grid(i)) > 0) {
+                stopGrid(i);
+
+                awaitPartitionMapExchange();
+
+                break;
+            }
+        }
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        assertEquals("Only one metric registry can persist for one service instance", expectedCnt,
+            G.allGrids().stream().map(grid -> metricsCnt((IgniteEx)grid)).mapToInt(Integer::intValue).sum());
+    }
+
+    /** Tests service metrics for single service instance. */
+    @Test
+    public void testServiceMetricsSingle() throws Throwable {
+        testServiceMetrics(1, 1, 1, 1);
+    }
+
+    /** Tests service metrics for multy service instance: one per server. */
+    @Test
+    public void testServiceMetricsMulty() throws Throwable {
+        testServiceMetrics(3, 3, 3, 1);
+    }
+
+    /** Tests service metrics for multy service instance: fewer that servers and clients. */
+    @Test
+    public void testServiceMetricsMultyFew() throws Throwable {
+        testServiceMetrics(4, 3, 2, 1);
+    }
+
+    /** Tests service metrics for multy service instance: serveral instances per node. */
+    @Test
+    public void testServiceMetricsMultyDuplicated() throws Throwable {
+        testServiceMetrics(3, 2, 3, 3);
+    }
+
+    /** Tests service metrics for multy service instance: serveral instances per node, total fewer that servers. */
+    @Test
+    public void testServiceMetricsMultyFewDuplicated() throws Throwable {
+        testServiceMetrics(5, 4, 3, 2);
+    }
+
+    /**
+     * Invokes service in various ways: from clients, servers, etc. Checks these calls reflect in the metrics.
+     *
+     * @param serverCnt Number of server nodes.
+     * @param clientCnt Number of client nodes.
+     * @param perClusterCnt Number of service instances per cluster.
+     * @param perNodeCnt Number of service instances per node.
+     */
+    private void testServiceMetrics(int serverCnt, int clientCnt, int perClusterCnt, int perNodeCnt) throws Throwable {
+        List<IgniteEx> servers = startGrids(serverCnt, false);
+
+        List<IgniteEx> clients = startGrids(clientCnt, true);
+
+        servers.get(0).services().deploy(serviceCfg(MyServiceFactory.create(), perClusterCnt, perNodeCnt));
+
+        awaitPartitionMapExchange();
+
+        List<MyService> serverStickyProxies = servers.stream()
+            .map(ignite -> (MyService)ignite.services().serviceProxy(SRVC_NAME, MyService.class, true))
+            .collect(Collectors.toList());
+
+        List<MyService> clientStickyProxies = clients.stream()
+            .map(ignite -> (MyService)ignite.services().serviceProxy(SRVC_NAME, MyService.class, true))
+            .collect(Collectors.toList());
+
+        long invokeCollector = 0;
+
+        // Call service through the server proxies.
+        for (int i = 0; i < INVOKE_CNT; ++i) {
+            // Call from server.
+            IgniteEx ignite = servers.get(i % servers.size());
+
+            callService4Times(ignite, serverStickyProxies.get(i % serverStickyProxies.size()));
+
+            // Call from client.
+            ignite = clients.get(i % clients.size());
+
+            callService4Times(ignite, clientStickyProxies.get(i % clientStickyProxies.size()));
+
+            invokeCollector += 8;
+        }
+
+        long invokesInMetrics = 0;
+
+        // Calculate and check invocations within the metrics.
+        for (IgniteEx ignite : servers) {
+            ReadOnlyMetricRegistry metrics = findMetricRegistry(ignite.context().metric(), SRVC_NAME);
+
+            // Metrics may not be deployed on this server node.
+            if (metrics == null)
+                continue;
+
+            for (Metric metric : metrics) {
+                if (metric instanceof HistogramMetric)
+                    invokesInMetrics += sumHistogramEntries((HistogramMetric)metric);
+            }
+        }
+
+        // Compare calls number and metrics number.
+        assertEquals("Calculated wrong service invocation number.", invokesInMetrics, invokeCollector);
+    }
+
+    /** Expose ignite-references of the nodes as list. */
+    private List<IgniteEx> startGrids(int cnt, boolean client) throws Exception {
+        List<IgniteEx> grids = new ArrayList<>(cnt);
+
+        for (int i = 0; i < cnt; ++i)
+            grids.add(client ? startClientGrid(gridNum++) : startGrid(gridNum++));
+
+        return grids;
+    }
+
+    /**
+     * Executes 2 calls for {@link MyService} though unsticky proxy and 2 calls to {@code extraSrvc}. Total 4 are
+     * suposed to present in the metrics.
+     *
+     * @param ignite Server or client node.
+     * @param extraSrvc Extra service instance or proxy to call.
+     */
+    private static void callService4Times(IgniteEx ignite, MyService extraSrvc) {
+        MyService srvc = ignite.services().serviceProxy(SRVC_NAME, MyService.class, false);
+
+        srvc.hello();
+
+        srvc.hello(1);
+
+        extraSrvc.hello();
+
+        extraSrvc.hello(2);
+    }
+
+    /** Provides test service configuration. */
+    private static ServiceConfiguration serviceCfg(Service srvc, int perClusterCnt, int perNodeCnt) {
+        ServiceConfiguration svcCfg = new ServiceConfiguration();
+
+        svcCfg.setName(SRVC_NAME);
+        svcCfg.setService(srvc);
+        svcCfg.setMaxPerNodeCount(perNodeCnt);
+        svcCfg.setTotalCount(perClusterCnt);
+        svcCfg.setStatisticsEnabled(true);
+
+        return svcCfg;
+    }
+
+    /** @return Number of metrics contained in metric registry of the test service. */
+    private static int metricsCnt(IgniteEx ignite) {
+        return Iterables.size(ignite.context().metric().registry(serviceMetricRegistryName(SRVC_NAME)));
+    }
+
+    /**
+     * Count total of histogram values.
+     *
+     * @param histogram Histogram to traverse.
+     * @return Sum of all entries of {@code histogram} buckets.
+     */
+    private static long sumHistogramEntries(HistogramMetric histogram) {
+        if (histogram == null)
+            return 0;
+
+        long sum = 0;
+
+        for (int i = 0; i < histogram.value().length; ++i)
+            sum += histogram.value()[i];
+
+        return sum;
+    }
+
+    /** @return Metric registry if it is found in {@code metricMgr} by name {@code srvcName}. Null otherwise. */
+    private static ReadOnlyMetricRegistry findMetricRegistry(GridMetricManager metricMgr, String srvcName) {
+        for (ReadOnlyMetricRegistry registry : metricMgr) {
+            if (registry.name().equals(serviceMetricRegistryName(srvcName)))
+                return registry;
+        }
+
+        return null;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
index edb6338..b081aa6 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
@@ -42,6 +42,7 @@
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.services.ServiceDeploymentException;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -216,19 +217,27 @@
 
         IgniteFuture<?> fut2 = svcs2.future();
 
-        info("Deployed service: " + name);
+        Exception err1 = null;
 
-        fut1.get();
-
-        info("Finished waiting for service future: " + name);
+        try {
+            fut1.get();
+        }
+        catch (ServiceDeploymentException e) {
+            if (e.getMessage().contains("Failed to deploy some services."))
+                err1 = e;
+            else
+                throw new IllegalStateException("An unexpeted error caught while deploying service.", e);
+        }
 
         try {
             fut2.get();
 
-            fail("Failed to receive mismatching configuration exception.");
+            if (err1 == null)
+                fail("Failed to receive mismatching configuration exception.");
         }
-        catch (IgniteException e) {
-            info("Received mismatching configuration exception: " + e.getMessage());
+        catch (Exception e) {
+            if (!e.getMessage().contains("Failed to deploy some service"))
+                throw new IllegalStateException("An unexpeted error caught while concurrent deploying.", e);
         }
     }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
index 76e02bf..b42c413 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.service;
 
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
@@ -26,6 +28,7 @@
 import org.apache.ignite.internal.util.typedef.PA;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
@@ -39,6 +42,13 @@
         return 4;
     }
 
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        grid(0).services().cancelAll();
+    }
+
     /**
      * @throws Exception If failed.
      */
@@ -240,15 +250,117 @@
     }
 
     /**
+     * Checks local service without the statistics.
+     *
      * @throws Exception If failed.
      */
     @Test
-    public void testLocalProxyInvocation() throws Exception {
-        final String name = "testLocalProxyInvocation";
+    public void testLocalProxyInvocationWithoutStat() throws Exception {
+        checkLocalProxy(false);
+    }
 
-        final Ignite ignite = grid(0);
+    /**
+     * Checks local service with the statistics enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLocalProxyInvocationWithStat() throws Exception {
+        checkLocalProxy(true);
+    }
 
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
+    /**
+     * Checks remote non-sticky proxy without the statistics.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteNotStickProxyInvocationWithoutStat() throws Exception {
+        checkRemoteProxy(false, false);
+    }
+
+    /**
+     * Checks remote non-sticky proxy with the statistics enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteNotStickyProxyInvocationWithStat() throws Exception {
+        checkRemoteProxy(true, false);
+    }
+
+    /**
+     * Checks remote sticky proxy without the statistics.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteStickyProxyInvocationWithoutStat() throws Exception {
+        checkRemoteProxy(false, true);
+    }
+
+    /**
+     * Checks remote sticky proxy with the statistics enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteStickyProxyInvocationWithStat() throws Exception {
+        checkRemoteProxy(true, true);
+    }
+
+    /**
+     * Checks remote service proxy (node singleton) with or without the statistics.
+     *
+     * @param withStat If {@code true}, enables the service metrics, {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
+     * @param sticky If {@code true}, requests sticky proxy.
+     */
+    private void checkRemoteProxy(boolean withStat, boolean sticky) throws InterruptedException {
+        final String svcName = "remoteServiceTest";
+
+        deployNodeSingleton(svcName, withStat);
+
+        Ignite ignite = grid(0);
+
+        // Get remote proxy.
+        MapService<Integer, String> svc = ignite.services(ignite.cluster().forRemotes()).
+            serviceProxy(svcName, MapService.class, sticky);
+
+        assertFalse(svc instanceof Service);
+
+        assertTrue(Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class));
+
+        assertEquals(svc.size(), 0);
+
+        for (int i = 0; i < nodeCount(); i++)
+            svc.put(i, Integer.toString(i));
+
+        int size = 0;
+
+        for (ClusterNode n : ignite.cluster().forRemotes().nodes()) {
+            MapService<Integer, String> map = ignite.services(ignite.cluster().forNode(n)).
+                serviceProxy(svcName, MapService.class, sticky);
+
+            assertFalse(map instanceof Service);
+
+            assertTrue(Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class));
+
+            if (map.size() != 0)
+                size += map.size();
+        }
+
+        assertEquals(nodeCount(), size);
+    }
+
+    /**
+     * Checks local service (node singleton) with or without statistics.
+     *
+     * @param withStat If {@code true}, enables the service metrics, {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
+     */
+    private void checkLocalProxy(boolean withStat) throws Exception {
+        final String svcName = "localProxyTest";
+
+        deployNodeSingleton(svcName, withStat);
 
         for (int i = 0; i < nodeCount(); i++) {
             final int idx = i;
@@ -260,11 +372,12 @@
                 @Override public boolean apply() {
                     MapService<Integer, String> svc = grid(idx)
                         .services()
-                        .serviceProxy(name, MapService.class, false);
+                        .serviceProxy(svcName, MapService.class, false);
 
                     ref.set(svc);
 
-                    return svc instanceof Service;
+                    return (Proxy.isProxyClass(svc.getClass())) &&
+                        Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class);
                 }
             }, 2000);
 
@@ -274,83 +387,30 @@
             ref.get().put(i, Integer.toString(i));
         }
 
-        MapService<Integer, String> map = ignite.services().serviceProxy(name, MapService.class, false);
+        MapService<Integer, String> map = grid(0).services().serviceProxy(svcName, MapService.class, false);
 
         for (int i = 0; i < nodeCount(); i++)
             assertEquals(1, map.size());
     }
 
     /**
-     * @throws Exception If failed.
+     * Deploys {@link MapServiceImpl} service over the cluster as node singleton.
+     *
+     * @param svcName Service name
+     * @param withStat If {@code true}, enabled the serive metrics {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
      */
-    @Test
-    public void testRemoteNotStickProxyInvocation() throws Exception {
-        final String name = "testRemoteNotStickProxyInvocation";
+    private void deployNodeSingleton(String svcName, boolean withStat) throws InterruptedException {
+        ServiceConfiguration svcCfg = new ServiceConfiguration();
 
-        final Ignite ignite = grid(0);
+        svcCfg.setName(svcName);
+        svcCfg.setMaxPerNodeCount(1);
+        svcCfg.setTotalCount(nodeCount());
+        svcCfg.setService(new MapServiceImpl<String, Integer>());
+        svcCfg.setStatisticsEnabled(withStat);
 
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
+        grid(0).services().deploy(svcCfg);
 
-        // Get remote proxy.
-        MapService<Integer, String> svc = ignite.services(ignite.cluster().forRemotes()).
-            serviceProxy(name, MapService.class, false);
-
-        // Make sure service is a local instance.
-        assertFalse(svc instanceof Service);
-
-        for (int i = 0; i < nodeCount(); i++)
-            svc.put(i, Integer.toString(i));
-
-        int size = 0;
-
-        for (ClusterNode n : ignite.cluster().forRemotes().nodes()) {
-            MapService<Integer, String> map = ignite.services(ignite.cluster().forNode(n)).
-                serviceProxy(name, MapService.class, false);
-
-            // Make sure service is a local instance.
-            assertFalse(map instanceof Service);
-
-            size += map.size();
-        }
-
-        assertEquals(nodeCount(), size);
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testRemoteStickyProxyInvocation() throws Exception {
-        final String name = "testRemoteStickyProxyInvocation";
-
-        final Ignite ignite = grid(0);
-
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
-
-        // Get remote proxy.
-        MapService<Integer, String> svc = ignite.services(ignite.cluster().forRemotes()).
-            serviceProxy(name, MapService.class, true);
-
-        // Make sure service is a local instance.
-        assertFalse(svc instanceof Service);
-
-        for (int i = 0; i < nodeCount(); i++)
-            svc.put(i, Integer.toString(i));
-
-        int size = 0;
-
-        for (ClusterNode n : ignite.cluster().forRemotes().nodes()) {
-            MapService<Integer, String> map = ignite.services(ignite.cluster().forNode(n)).
-                serviceProxy(name, MapService.class, false);
-
-            // Make sure service is a local instance.
-            assertFalse(map instanceof Service);
-
-            if (map.size() != 0)
-                size += map.size();
-        }
-
-        assertEquals(nodeCount(), size);
+        awaitPartitionMapExchange();
     }
 
     /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallContextTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallContextTest.java
index 20fbcb6..c2ccd86 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallContextTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallContextTest.java
@@ -110,6 +110,21 @@
     }
 
     /**
+     * Check proxy creation with an invalid implementation.
+     */
+    @Test
+    public void testInvalidContextImplementation() {
+        ServiceCallContext callCtx = new ServiceCallContext() {
+            @Override public String attribute(String name) { return null; }
+
+            @Override public byte[] binaryAttribute(String name) { return null; }
+        };
+
+        GridTestUtils.assertThrowsAnyCause(log, () -> grid(0).services().serviceProxy(SVC_NAME, TestService.class,
+            sticky, callCtx), IllegalArgumentException.class, "\"callCtx\" has an invalid type.");
+    }
+
+    /**
      * Check context attribute.
      */
     @Test
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
index 251b438..325f145 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
@@ -32,6 +32,13 @@
     int hello();
 
     /**
+     * @return Given {@code helloValue}.
+     */
+    default int hello(int helloValue) {
+        return helloValue;
+    }
+
+    /**
      * hashCode() method with a dummy argument.
      *
      * @param dummy Argument.
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/EchoServer.java b/modules/core/src/test/java/org/apache/ignite/internal/util/EchoServer.java
new file mode 100644
index 0000000..21cc73c
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/EchoServer.java
@@ -0,0 +1,139 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * A simple TCP server that echoes back every byte that it receives. Can be used when some protocol-neutral property
+ * of a TCP client (like socket closure handling) needs to be tested.
+ *
+ * This server must be operated (closed, address obtained, etc) from the same thread in which it was started.
+ */
+class EchoServer implements AutoCloseable {
+    /***/
+    private final int port;
+
+    /***/
+    private final ExecutorService acceptorExecutor = Executors.newSingleThreadExecutor();
+
+    /***/
+    private final ExecutorService workersExecutor = Executors.newCachedThreadPool();
+
+    /***/
+    private ServerSocket serverSocket;
+
+    /***/
+    private volatile boolean running;
+
+    /***/
+    EchoServer(int port) {
+        this.port = port;
+    }
+
+    /***/
+    public void start() throws IOException {
+        running = true;
+
+        serverSocket = new ServerSocket(port);
+
+        acceptorExecutor.submit(new Acceptor());
+    }
+
+    /***/
+    public void stop() throws IOException {
+        assert running : "Not started yet";
+
+        running = false;
+
+        serverSocket.close();
+
+        IgniteUtils.shutdownNow(getClass(), acceptorExecutor, null);
+        IgniteUtils.shutdownNow(getClass(), workersExecutor, null);
+    }
+
+    /***/
+    public SocketAddress localSocketAddress() {
+        assert serverSocket != null;
+
+        return serverSocket.getLocalSocketAddress();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void close() throws IOException {
+        stop();
+    }
+
+    /***/
+    private class Acceptor implements Runnable {
+        /** {@inheritDoc} */
+        @Override public void run() {
+            while (running) {
+                Socket socket = acceptConnection();
+                workersExecutor.submit(new Worker(socket));
+            }
+        }
+
+        /***/
+        private Socket acceptConnection() {
+            try {
+                return serverSocket.accept();
+            }
+            catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+    }
+
+    /***/
+    private class Worker implements Runnable {
+        /***/
+        private final Socket socket;
+
+        /***/
+        private Worker(Socket socket) {
+            this.socket = socket;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void run() {
+            try (Socket ignored = socket) {
+                InputStream is = socket.getInputStream();
+                OutputStream os = socket.getOutputStream();
+
+                while (running) {
+                    int ch = is.read();
+                    if (ch < 0) {
+                        break;
+                    }
+                    os.write(ch);
+                }
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsUnitTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsUnitTest.java
new file mode 100644
index 0000000..fd8c237
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsUnitTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.apache.ignite.testframework.ListeningTestLogger;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Unit tests for {@link IgniteUtils}.
+ */
+public class IgniteUtilsUnitTest {
+    /***/
+    private static final int PORT = 5555;
+
+    /***/
+    private final List<String> logMessages = new CopyOnWriteArrayList<>();
+
+    /***/
+    @Test
+    public void shouldNotProduceWarningsWhenClosingAnAlreadyClosedSocket() throws Exception {
+        try (EchoServer server = new EchoServer(PORT)) {
+            server.start();
+
+            try (SocketChannel channel = connectTo(server)) {
+                // closing first time
+                channel.close();
+
+                // now close second time and collect logs
+                IgniteUtils.close(channel.socket(), logMessagesCollector());
+            }
+        }
+
+        assertThat(logMessages, is(empty()));
+    }
+
+    /***/
+    private SocketChannel connectTo(EchoServer server) throws IOException {
+        return SocketChannel.open(server.localSocketAddress());
+    }
+
+    /***/
+    private ListeningTestLogger logMessagesCollector() {
+        ListeningTestLogger log = new ListeningTestLogger();
+
+        log.registerListener(logMessages::add);
+
+        return log;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/nio/GridNioServerTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/nio/GridNioServerTest.java
new file mode 100644
index 0000000..9020ca8
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/nio/GridNioServerTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.util.nio;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.testframework.ListeningTestLogger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.not;
+
+/**
+ * Unit tests for {@link GridNioServer}.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class GridNioServerTest {
+    /***/
+    private final List<String> logMessages = new CopyOnWriteArrayList<>();
+
+    /***/
+    private static final int PORT = 5555;
+
+    /***/
+    @Mock
+    private GridNioServerListener<Object> noOpListener;
+
+    /***/
+    @Test
+    public void shouldNotLogWarningsOnKeyClose() throws Exception {
+        GridNioServer<Object> server = startServerCollectingLogMessages();
+
+        try (Socket ignored = openSocketTo(server)) {
+            server.stop();
+        }
+
+        assertThat(logMessages, not(hasItem(containsString("Failed to shutdown socket"))));
+        assertThat(logMessages, not(hasItem(containsString("ClosedChannelException"))));
+    }
+
+    /***/
+    private GridNioServer<Object> startServerCollectingLogMessages() throws IgniteCheckedException,
+        UnknownHostException {
+        GridNioServer<Object> server = GridNioServer.builder()
+            .address(InetAddress.getLocalHost())
+            .port(PORT)
+            .selectorCount(1)
+            .listener(noOpListener)
+            .logger(logMessagesCollector())
+            .build();
+
+        server.start();
+
+        return server;
+    }
+
+    /***/
+    private ListeningTestLogger logMessagesCollector() {
+        ListeningTestLogger log = new ListeningTestLogger();
+
+        log.registerListener(logMessages::add);
+
+        return log;
+    }
+
+    /***/
+    private Socket openSocketTo(GridNioServer<Object> server) throws IOException {
+        return new Socket(server.localAddress().getAddress(), server.port());
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
index 625760c..0436d4c 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
@@ -39,9 +39,11 @@
 import org.apache.ignite.compute.ComputeJobAdapter;
 import org.apache.ignite.compute.ComputeJobResult;
 import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.platform.model.ACL;
+import org.apache.ignite.platform.model.AccessLevel;
 import org.apache.ignite.platform.model.Account;
 import org.apache.ignite.platform.model.Address;
 import org.apache.ignite.platform.model.Department;
@@ -412,14 +414,29 @@
         }
 
         /** */
-        public BinaryObject[] testBinaryObjectArray(BinaryObject[] arg) {
-            for (int i = 0; i < arg.length; i++) {
-                int field = arg[i].field("Field");
+        public BinaryObject[] testBinaryObjectArray(Object arg0) {
+            Object[] arg;
 
-                arg[i] = arg[i].toBuilder().setField("Field", field + 1).build();
+            if (BinaryArray.useBinaryArrays()) {
+                assertTrue(arg0 instanceof BinaryArray);
+
+                arg = ((BinaryArray)arg0).array();
+            }
+            else {
+                assertTrue(arg0 instanceof Object[]);
+
+                arg = (Object[])arg0;
             }
 
-            return arg;
+            BinaryObject[] res = new BinaryObject[arg.length];
+
+            for (int i = 0; i < arg.length; i++) {
+                int field = ((BinaryObject)arg[i]).field("Field");
+
+                res[i] = ((BinaryObject)arg[i]).toBuilder().setField("Field", field + 1).build();
+            }
+
+            return res;
         }
 
         /** */
@@ -575,8 +592,8 @@
         /** */
         public User[] testUsers() {
             return new User[] {
-                new User(1, ACL.ALLOW, new Role("admin")),
-                new User(2, ACL.DENY, new Role("user"))
+                new User(1, ACL.ALLOW, new Role("admin", AccessLevel.SUPER)),
+                new User(2, ACL.DENY, new Role("user", AccessLevel.USER))
             };
         }
 
@@ -640,6 +657,11 @@
         }
 
         /** */
+        public Object testRoundtrip(Object x) {
+            return x;
+        }
+
+        /** */
         public void sleep(long delayMs) {
             try {
                 U.sleep(delayMs);
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
index efe8841..b08fd7f 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallThinTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallThinTask.java
@@ -22,6 +22,7 @@
 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.services.ServiceCallContext;
 import org.apache.ignite.testframework.GridTestUtils;
 
 /**
@@ -47,7 +48,9 @@
 
         /** {@inheritDoc} */
         @Override TestPlatformService serviceProxy() {
-            return client.services().serviceProxy(srvcName, TestPlatformService.class);
+            ServiceCallContext callCtx = ServiceCallContext.builder().put("attr", "value").build();
+
+            return client.services().serviceProxy(srvcName, TestPlatformService.class, callCtx);
         }
 
         /** {@inheritDoc} */
@@ -71,10 +74,5 @@
                 return null;
             }, ClientException.class, "Failed to invoke platform service");
         }
-
-        /** {@inheritDoc} */
-        @Override protected void checkContextAttribute(TestPlatformService srv) {
-            // TODO IGNITE-15829 Remove this method override.
-        }
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformSetUseBinaryArrayTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformSetUseBinaryArrayTask.java
new file mode 100644
index 0000000..1579735
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformSetUseBinaryArrayTask.java
@@ -0,0 +1,71 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteSystemProperties;
+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.internal.binary.BinaryArray;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+
+/**
+ * Task to set {@link IgniteSystemProperties#IGNITE_USE_BINARY_ARRAYS} to {@code true}.
+ */
+public class PlatformSetUseBinaryArrayTask extends ComputeTaskAdapter<Boolean, Void> {
+    /** {@inheritDoc} */
+    @Override public @NotNull Map<? extends ComputeJob, ClusterNode> map(List<ClusterNode> subgrid,
+        @Nullable Boolean useTypedArr) throws IgniteException {
+        Map<ComputeJob, ClusterNode> jobs = new HashMap<>();
+
+        for (ClusterNode node : subgrid)
+            jobs.putIfAbsent(new SetUseTypedArrayJob(useTypedArr), node);
+
+        return jobs;
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable Void reduce(List<ComputeJobResult> results) throws IgniteException {
+        return null;
+    }
+
+    /** Job. */
+    private static class SetUseTypedArrayJob extends ComputeJobAdapter {
+        /** {@inheritDoc} */
+        public SetUseTypedArrayJob(@Nullable Object arg) {
+            super(arg);
+        }
+
+        /** {@inheritDoc} */
+        @Nullable @Override public Object execute() {
+            System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(argument(0)));
+            BinaryArray.initUseBinaryArrays();
+
+            return null;
+        }
+    }
+}
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/core/src/test/java/org/apache/ignite/platform/model/AccessLevel.java
similarity index 73%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/core/src/test/java/org/apache/ignite/platform/model/AccessLevel.java
index a5b6d8f..f14e3a2 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/model/AccessLevel.java
@@ -15,18 +15,13 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
+package org.apache.ignite.platform.model;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+/** Test enum. */
+public enum AccessLevel {
+    /** */
+    USER,
 
-/**
- * Zookeeper IP Finder tests.
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
-
+    /** */
+    SUPER
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/model/Role.java b/modules/core/src/test/java/org/apache/ignite/platform/model/Role.java
index f47eddb..ad8b3d3 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/model/Role.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/model/Role.java
@@ -23,8 +23,12 @@
     String name;
 
     /** */
-    public Role(String name) {
+    AccessLevel accessLevel;
+
+    /** */
+    public Role(String name, AccessLevel accessLevel) {
         this.name = name;
+        this.accessLevel = accessLevel;
     }
 
     /** */
@@ -36,4 +40,14 @@
     public void setName(String name) {
         this.name = name;
     }
+
+    /** */
+    public AccessLevel getAccessLevel() {
+        return accessLevel;
+    }
+
+    /** */
+    public void setAccessLevel(AccessLevel accessLevel) {
+        this.accessLevel = accessLevel;
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/session/GridSessionCheckpointSelfTest.java b/modules/core/src/test/java/org/apache/ignite/session/GridSessionCheckpointSelfTest.java
index 24c80de..b1d7fbb 100644
--- a/modules/core/src/test/java/org/apache/ignite/session/GridSessionCheckpointSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/session/GridSessionCheckpointSelfTest.java
@@ -22,9 +22,7 @@
 import org.apache.ignite.internal.binary.BinaryCachingMetadataHandler;
 import org.apache.ignite.internal.binary.BinaryContext;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.logger.NullLogger;
-import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
 import org.apache.ignite.spi.checkpoint.cache.CacheCheckpointSpi;
 import org.apache.ignite.spi.checkpoint.jdbc.JdbcCheckpointSpi;
@@ -101,13 +99,13 @@
         cfg.setCheckpointSpi(spi);
 
         if (cfg.getMarshaller() instanceof BinaryMarshaller) {
-            BinaryContext ctx = new BinaryContext(BinaryCachingMetadataHandler.create(), cfg, new NullLogger());
+            BinaryMarshaller marsh = (BinaryMarshaller)cfg.getMarshaller();
 
-            Marshaller marsh = cfg.getMarshaller();
+            BinaryContext ctx = new BinaryContext(BinaryCachingMetadataHandler.create(), cfg, new NullLogger());
 
             marsh.setContext(new MarshallerContextTestImpl(null));
 
-            IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, cfg);
+            marsh.setBinaryContext(ctx, cfg);
         }
 
         GridSessionCheckpointSelfTest.spi = spi;
diff --git a/modules/core/src/test/java/org/apache/ignite/spi/collision/GridTestCollisionTaskSession.java b/modules/core/src/test/java/org/apache/ignite/spi/collision/GridTestCollisionTaskSession.java
index 3a7d757..47c00d6 100644
--- a/modules/core/src/test/java/org/apache/ignite/spi/collision/GridTestCollisionTaskSession.java
+++ b/modules/core/src/test/java/org/apache/ignite/spi/collision/GridTestCollisionTaskSession.java
@@ -21,28 +21,33 @@
 import java.util.Map;
 import java.util.UUID;
 import org.apache.ignite.compute.ComputeJobSibling;
-import org.apache.ignite.compute.ComputeTaskSession;
 import org.apache.ignite.compute.ComputeTaskSessionAttributeListener;
 import org.apache.ignite.compute.ComputeTaskSessionScope;
+import org.apache.ignite.internal.GridTaskSessionInternal;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Test collision task session.
  */
-public class GridTestCollisionTaskSession implements ComputeTaskSession {
-    /** */
+public class GridTestCollisionTaskSession implements GridTaskSessionInternal {
+    /** Task priority. */
     private Integer pri = 0;
 
-    /** */
+    /** Task priority attribute key.  */
     private String priAttrKey;
 
-    /** */
+    /**
+     * Default constructor.
+     */
     public GridTestCollisionTaskSession() {
         // No-op.
     }
 
     /**
+     * Constructor.
+     *
      * @param pri Priority.
      * @param priAttrKey Priority attribute key.
      */
@@ -55,81 +60,68 @@
 
     /** {@inheritDoc} */
     @Override public UUID getTaskNodeId() {
-        assert false;
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public <K, V> V waitForAttribute(K key, long timeout) {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public boolean waitForAttribute(Object key, Object val, long timeout) throws InterruptedException {
-        assert false : "Not implemented";
-
-        return false;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public Map<?, ?> waitForAttributes(Collection<?> keys, long timeout) {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
-    @Override public boolean waitForAttributes(Map<?, ?> attrs, long timeout) throws InterruptedException {
-        assert false : "Not implemented";
-
-        return false;
+    @Override public boolean waitForAttributes(Map<?, ?> attrs, long timeout) {
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public void saveCheckpoint(String key, Object state) {
-        assert false : "Not implemented";
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public void saveCheckpoint(String key, Object state, ComputeTaskSessionScope scope, long timeout) {
-        assert false : "Not implemented";
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
-    @Override public void saveCheckpoint(String key, Object state, ComputeTaskSessionScope scope, long timeout,
-        boolean overwrite) {
-        assert false : "Not implemented";
+    @Override public void saveCheckpoint(
+        String key,
+        Object state,
+        ComputeTaskSessionScope scope,
+        long timeout,
+        boolean overwrite
+    ) {
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public <T> T loadCheckpoint(String key) {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public boolean removeCheckpoint(String key) {
-        assert false : "Not implemented";
-
-        return false;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public String getTaskName() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public IgniteUuid getId() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
@@ -139,16 +131,12 @@
 
     /** {@inheritDoc} */
     @Override public ClassLoader getClassLoader() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public Collection<ComputeJobSibling> getJobSiblings() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
@@ -158,14 +146,12 @@
 
     /** {@inheritDoc} */
     @Override public ComputeJobSibling getJobSibling(IgniteUuid jobId) {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public void setAttribute(Object key, Object val) {
-        assert false : "Not implemented";
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
@@ -178,33 +164,27 @@
 
     /** {@inheritDoc} */
     @Override public void setAttributes(Map<?, ?> attrs) {
-        assert false : "Not implemented";
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public Map<Object, Object> getAttributes() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public void addAttributeListener(ComputeTaskSessionAttributeListener lsnr, boolean rewind) {
-        assert false : "Not implemented";
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public boolean removeAttributeListener(ComputeTaskSessionAttributeListener lsnr) {
-        assert false : "Not implemented";
-
-        return false;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /** {@inheritDoc} */
     @Override public IgniteFuture<?> mapFuture() {
-        assert false : "Not implemented";
-
-        return null;
+        throw new UnsupportedOperationException("Not implemented");
     }
 
     /**
@@ -239,14 +219,42 @@
     }
 
     /** {@inheritDoc} */
+    @Override public String getCheckpointSpi() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable IgniteUuid getJobId() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isTaskNode() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onClosed() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isClosed() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridTaskSessionInternal session() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isFullSupport() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
     @Override public String toString() {
-        StringBuilder buf = new StringBuilder();
-
-        buf.append(getClass().getName());
-        buf.append(" [priority=").append(pri);
-        buf.append(", priorityAttrKey='").append(priAttrKey).append('\'');
-        buf.append(']');
-
-        return buf.toString();
+        return getClass().getName() + " [priority=" + pri + ", priorityAttrKey='" + priAttrKey + '\'' + ']';
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
index 3bc9a0d..38711a5 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
@@ -65,12 +65,14 @@
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BooleanSupplier;
+import java.util.stream.Collectors;
 import javax.cache.CacheException;
 import javax.cache.configuration.Factory;
 import javax.management.Attribute;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
+import com.google.common.collect.Lists;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
@@ -1130,6 +1132,16 @@
      * @param task Runnable.
      * @return Future with task result.
      */
+    public static IgniteInternalFuture runAsync(final RunnableX task) {
+        return runAsync(task, "async-runnable-runner");
+    }
+
+    /**
+     * Runs runnable task asyncronously.
+     *
+     * @param task Runnable.
+     * @return Future with task result.
+     */
     public static IgniteInternalFuture runAsync(final Runnable task, String threadName) {
         return runAsync(() -> {
             task.run();
@@ -1139,6 +1151,20 @@
     }
 
     /**
+     * Runs runnable task asyncronously.
+     *
+     * @param task Runnable.
+     * @return Future with task result.
+     */
+    public static IgniteInternalFuture runAsync(final RunnableX task, String threadName) {
+        return runAsync(() -> {
+            task.run();
+
+            return null;
+        }, threadName);
+    }
+
+    /**
      * Runs callable task asyncronously.
      *
      * @param task Callable.
@@ -2339,6 +2365,13 @@
         assertFalse(state + " isn't inactive state", state.active());
     }
 
+    /** @return Cartesian product of collections. See {@link Lists#cartesianProduct(List)}. */
+    public static Collection<Object[]> cartesianProduct(Collection<?>... c) {
+        List<List<?>> lists = F.asList(c).stream().map(ArrayList::new).collect(Collectors.toList());
+
+        return F.transform(Lists.cartesianProduct(lists), List::toArray);
+    }
+
     /** Test parameters scale factor util. */
     private static class ScaleFactorUtil {
         /** Test speed scale factor property name. */
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridAbstractTest.java
index 9a06f28..cea4f40 100755
--- a/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridAbstractTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridAbstractTest.java
@@ -1947,7 +1947,7 @@
 
         marsh.setContext(new MarshallerContextTestImpl());
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, cfg);
+        marsh.setBinaryContext(ctx, cfg);
 
         return marsh;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridTestBinaryMarshaller.java b/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridTestBinaryMarshaller.java
index 663ccd9..3eebd49 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridTestBinaryMarshaller.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/junits/GridTestBinaryMarshaller.java
@@ -28,7 +28,6 @@
 import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.binary.BinaryObjectImpl;
 import org.apache.ignite.internal.binary.GridBinaryMarshaller;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.MarshallerContextTestImpl;
@@ -86,7 +85,7 @@
 
         marsh.setContext(marshCtx);
 
-        IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, iCfg);
+        marsh.setBinaryContext(ctx, iCfg);
 
         return marsh;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteTestResources.java b/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteTestResources.java
index b418d21..6325a40 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteTestResources.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteTestResources.java
@@ -32,7 +32,6 @@
 import org.apache.ignite.internal.binary.BinaryContext;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessor;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.logger.NullLogger;
 import org.apache.ignite.marshaller.Marshaller;
@@ -273,10 +272,11 @@
         marsh.setContext(new MarshallerContextTestImpl());
 
         if (marsh instanceof BinaryMarshaller) {
+            BinaryMarshaller binaryMarsh = (BinaryMarshaller)marsh;
+
             BinaryContext ctx =
                 new BinaryContext(BinaryCachingMetadataHandler.create(), new IgniteConfiguration(), new NullLogger());
-
-            IgniteUtils.invoke(BinaryMarshaller.class, marsh, "setBinaryContext", ctx, new IgniteConfiguration());
+            binaryMarsh.setBinaryContext(ctx, new IgniteConfiguration());
         }
 
         return marsh;
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/junits/common/GridCommonAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/testframework/junits/common/GridCommonAbstractTest.java
index cf7a573..2e6bcfe 100755
--- a/modules/core/src/test/java/org/apache/ignite/testframework/junits/common/GridCommonAbstractTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/junits/common/GridCommonAbstractTest.java
@@ -1878,6 +1878,22 @@
     }
 
     /**
+     * @param exp Expected.
+     * @param act Actual.
+     */
+    protected static void assertEqualsMaps(Map<?, ?> exp, Map<?, ?> act) {
+        if (exp.size() != act.size())
+            fail("Maps are not equal:\nExpected:\t" + exp + "\nActual:\t" + act);
+
+        for (Map.Entry<?, ?> e : exp.entrySet()) {
+            if (!act.containsKey(e.getKey()))
+                fail("Maps are not equal (missing key " + e.getKey() + "):\nExpected:\t" + exp + "\nActual:\t" + act);
+            else if (!F.eq(e.getValue(), act.get(e.getKey())))
+                fail("Maps are not equal (key " + e.getKey() + "):\nExpected:\t" + exp + "\nActual:\t" + act);
+        }
+    }
+
+    /**
      * @param ignite Ignite instance.
      * @param clo Closure.
      * @return Result of closure execution.
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/junits/multijvm/IgniteCacheProcessProxy.java b/modules/core/src/test/java/org/apache/ignite/testframework/junits/multijvm/IgniteCacheProcessProxy.java
index b71d37d..68d8c4c 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/junits/multijvm/IgniteCacheProcessProxy.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/junits/multijvm/IgniteCacheProcessProxy.java
@@ -39,6 +39,7 @@
 import org.apache.ignite.cache.CacheEntryProcessor;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.cache.ReadRepairStrategy;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.Query;
 import org.apache.ignite.cache.query.QueryCursor;
@@ -140,7 +141,7 @@
     }
 
     /** {@inheritDoc} */
-    @Override public IgniteCache<K, V> withReadRepair() {
+    @Override public IgniteCache<K, V> withReadRepair(ReadRepairStrategy strategy) {
         throw new UnsupportedOperationException("Method should be supported.");
     }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
index b526357..07034f1 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.testsuites;
 
 import org.apache.ignite.internal.binary.BinaryArrayIdentityResolverSelfTest;
+import org.apache.ignite.internal.binary.BinaryArraySelfTest;
 import org.apache.ignite.internal.binary.BinaryBasicIdMapperSelfTest;
 import org.apache.ignite.internal.binary.BinaryBasicNameMapperSelfTest;
 import org.apache.ignite.internal.binary.BinaryConfigurationConsistencySelfTest;
@@ -122,6 +123,7 @@
     BinaryFooterOffsetsHeapSelfTest.class,
     BinaryFooterOffsetsOffheapSelfTest.class,
     BinaryEnumsSelfTest.class,
+    BinaryArraySelfTest.class,
     GridDefaultBinaryMappersBinaryMetaDataSelfTest.class,
     GridSimpleLowerCaseBinaryMappersBinaryMetaDataSelfTest.class,
     GridBinaryAffinityKeySelfTest.class,
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheConsistencySelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheConsistencySelfTestSuite.java
new file mode 100644
index 0000000..82bd4b8
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheConsistencySelfTestSuite.java
@@ -0,0 +1,55 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.internal.processors.cache.consistency.inmem.AtomicReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ExplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ImplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ReplicatedExplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.ReplicatedImplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.SingleBackupExplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.inmem.SingleBackupImplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.persistence.PdsAtomicReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.persistence.PdsExplicitTransactionalReadRepairTest;
+import org.apache.ignite.internal.processors.cache.consistency.persistence.PdsImplicitTransactionalReadRepairTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Test suite for cache consistency checks.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    // Inmem
+    AtomicReadRepairTest.class,
+    ExplicitTransactionalReadRepairTest.class,
+    ImplicitTransactionalReadRepairTest.class,
+
+    // PDS
+    PdsAtomicReadRepairTest.class,
+    PdsExplicitTransactionalReadRepairTest.class,
+    PdsImplicitTransactionalReadRepairTest.class,
+
+    // Special (inmem)
+    ReplicatedExplicitTransactionalReadRepairTest.class,
+    ReplicatedImplicitTransactionalReadRepairTest.class,
+    SingleBackupExplicitTransactionalReadRepairTest.class,
+    SingleBackupImplicitTransactionalReadRepairTest.class,
+})
+public class IgniteCacheConsistencySelfTestSuite {
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheDataStructuresSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheDataStructuresSelfTestSuite.java
index dc0810d..895e841 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheDataStructuresSelfTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheDataStructuresSelfTestSuite.java
@@ -18,13 +18,6 @@
 package org.apache.ignite.testsuites;
 
 import org.apache.ignite.internal.processors.cache.AtomicCacheAffinityConfigurationTest;
-import org.apache.ignite.internal.processors.cache.consistency.AtomicReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.ExplicitTransactionalReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.ImplicitTransactionalReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.ReplicatedExplicitTransactionalReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.ReplicatedImplicitTransactionalReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.SingleBackupExplicitTransactionalReadRepairTest;
-import org.apache.ignite.internal.processors.cache.consistency.SingleBackupImplicitTransactionalReadRepairTest;
 import org.apache.ignite.internal.processors.cache.datastructures.GridCacheQueueCleanupSelfTest;
 import org.apache.ignite.internal.processors.cache.datastructures.GridCacheQueueClientDisconnectTest;
 import org.apache.ignite.internal.processors.cache.datastructures.GridCacheQueueMultiNodeConsistencySelfTest;
@@ -208,16 +201,6 @@
 
     IgniteCacheDataStructuresBinarySelfTestSuite.class,
 
-    ImplicitTransactionalReadRepairTest.class,
-    SingleBackupImplicitTransactionalReadRepairTest.class,
-    ReplicatedImplicitTransactionalReadRepairTest.class,
-
-    AtomicReadRepairTest.class,
-
-    ExplicitTransactionalReadRepairTest.class,
-    SingleBackupExplicitTransactionalReadRepairTest.class,
-    ReplicatedExplicitTransactionalReadRepairTest.class,
-
     IgniteAtomicLongClusterReadOnlyTest.class,
     IgniteAtomicReferenceClusterReadOnlyTest.class,
     IgniteAtomicSequenceClusterReadOnlyTest.class,
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite7.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite7.java
index 3b942fb..b0cdd37 100755
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite7.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite7.java
@@ -46,6 +46,7 @@
 import org.apache.ignite.internal.processors.cache.distributed.dht.GridCacheDhtPreloadDelayedWithPersistenceSelfTest;
 import org.apache.ignite.internal.processors.cache.distributed.dht.GridCacheDhtPreloadWaitForBackupsWithPersistenceTest;
 import org.apache.ignite.internal.processors.cache.distributed.dht.IgniteCacheStartWithLoadTest;
+import org.apache.ignite.internal.processors.cache.distributed.dht.WaitForBackupsOnShutdownSystemPropertyTest;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.BlockedEvictionsTest;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.DelayedOwningDuringExchangeTest;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.EvictionWhilePartitionGroupIsReservedTest;
@@ -149,6 +150,7 @@
 
         GridTestUtils.addTestIfNeeded(suite, GridCacheDhtPreloadWaitForBackupsWithPersistenceTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, GracefulShutdownTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, WaitForBackupsOnShutdownSystemPropertyTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, TxWithKeyContentionSelfTest.class, ignoredTests);
 
         GridTestUtils.addTestIfNeeded(suite, GridCacheDhtPreloadDelayedWithPersistenceSelfTest.class, ignoredTests);
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteComputeGridTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteComputeGridTestSuite.java
index 61cc2a4..8ad91a8 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteComputeGridTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteComputeGridTestSuite.java
@@ -79,6 +79,10 @@
 import org.apache.ignite.internal.managers.checkpoint.GridCheckpointManagerSelfTest;
 import org.apache.ignite.internal.managers.checkpoint.GridCheckpointTaskSelfTest;
 import org.apache.ignite.internal.managers.communication.GridCommunicationManagerListenersSelfTest;
+import org.apache.ignite.internal.processors.compute.ComputeGridMonitorTest;
+import org.apache.ignite.internal.processors.compute.ComputeJobChangePriorityTest;
+import org.apache.ignite.internal.processors.compute.ComputeJobStatusTest;
+import org.apache.ignite.internal.processors.compute.ComputeTaskWithWithoutFullSupportTest;
 import org.apache.ignite.internal.processors.compute.IgniteComputeCustomExecutorConfigurationSelfTest;
 import org.apache.ignite.internal.processors.compute.IgniteComputeCustomExecutorSelfTest;
 import org.apache.ignite.internal.processors.compute.PublicThreadpoolStarvationTest;
@@ -172,7 +176,12 @@
 
     IgniteComputeJobOneThreadTest.class,
 
-    VisorManagementEventSelfTest.class
+    VisorManagementEventSelfTest.class,
+
+    ComputeGridMonitorTest.class,
+    ComputeJobChangePriorityTest.class,
+    ComputeJobStatusTest.class,
+    ComputeTaskWithWithoutFullSupportTest.class
 })
 public class IgniteComputeGridTestSuite {
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteKernalSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteKernalSelfTestSuite.java
index 70bce85..d30f803 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteKernalSelfTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteKernalSelfTestSuite.java
@@ -50,6 +50,7 @@
 import org.apache.ignite.internal.managers.discovery.IgniteTopologyPrintFormatSelfTest;
 import org.apache.ignite.internal.managers.events.GridEventStorageManagerInternalEventsSelfTest;
 import org.apache.ignite.internal.managers.events.GridEventStorageManagerSelfTest;
+import org.apache.ignite.internal.managers.events.LifecycleAwareListenerTest;
 import org.apache.ignite.internal.processors.cache.ClusterActiveStateChangeWithNodeOutOfBaselineTest;
 import org.apache.ignite.internal.processors.cluster.BaselineAutoAdjustInMemoryTest;
 import org.apache.ignite.internal.processors.cluster.BaselineAutoAdjustTest;
@@ -115,7 +116,8 @@
     NodeWithFilterRestartTest.class,
     ClusterActiveStateChangeWithNodeOutOfBaselineTest.class,
     IgniteNodeValidationFailedEventTest.class,
-    GridMutableLongTest.class
+    GridMutableLongTest.class,
+    LifecycleAwareListenerTest.class
 })
 public class IgniteKernalSelfTestSuite {
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteMarshallerSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteMarshallerSelfTestSuite.java
index 8e22bfb..48040d6 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteMarshallerSelfTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteMarshallerSelfTestSuite.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.testsuites;
 
+import org.apache.ignite.internal.direct.DirectMarshallingMessagesTest;
 import org.apache.ignite.internal.direct.stream.v2.DirectByteBufferStreamImplV2ByteOrderSelfTest;
 import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerEnumSelfTest;
 import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerNodeFailoverTest;
@@ -50,7 +51,8 @@
     DirectByteBufferStreamImplV2ByteOrderSelfTest.class,
     GridHandleTableSelfTest.class,
     OptimizedMarshallerPooledSelfTest.class,
-    MarshallerEnumDeadlockMultiJvmTest.class
+    MarshallerEnumDeadlockMultiJvmTest.class,
+    DirectMarshallingMessagesTest.class
 })
 public class IgniteMarshallerSelfTestSuite {
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite4.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite4.java
index afab955..e5cc346 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite4.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite4.java
@@ -36,6 +36,7 @@
 import org.apache.ignite.internal.processors.cache.distributed.rebalancing.SupplyPartitionHistoricallyWithReorderedUpdates;
 import org.apache.ignite.internal.processors.cache.persistence.CorruptedTreeFailureHandlingTest;
 import org.apache.ignite.internal.processors.cache.persistence.IgnitePdsCacheEntriesExpirationTest;
+import org.apache.ignite.internal.processors.cache.persistence.IgnitePdsCheckpointMapSnapshotTest;
 import org.apache.ignite.internal.processors.cache.persistence.IgnitePdsConsistencyOnDelayedPartitionOwning;
 import org.apache.ignite.internal.processors.cache.persistence.IgnitePdsContinuousRestartTestWithSharedGroupAndIndexes;
 import org.apache.ignite.internal.processors.cache.persistence.IgnitePdsDefragmentationEncryptionTest;
@@ -114,6 +115,7 @@
         GridTestUtils.addTestIfNeeded(suite, RebalanceCompleteDuringExchangeTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, ReleaseSegmentOnHistoricalRebalanceTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, AutoReleaseSegmentSelfTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, IgnitePdsCheckpointMapSnapshotTest.class, ignoredTests);
 
         // Page lock tracker tests.
         GridTestUtils.addTestIfNeeded(suite, PageLockTrackerManagerTest.class, ignoredTests);
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
index 64622ad..a1fe470 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.internal.processors.service.GridServiceDeployClusterReadOnlyModeTest;
 import org.apache.ignite.internal.processors.service.GridServiceDeploymentCompoundFutureSelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceDeploymentExceptionPropagationTest;
+import org.apache.ignite.internal.processors.service.GridServiceMetricsTest;
 import org.apache.ignite.internal.processors.service.GridServicePackagePrivateSelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceProcessorBatchDeploySelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceProcessorMultiNodeConfigSelfTest;
@@ -121,6 +122,7 @@
     GridServiceDeployClusterReadOnlyModeTest.class,
     GridServiceClusterReadOnlyModeTest.class,
     IgniteServiceCallContextTest.class,
+    GridServiceMetricsTest.class
 })
 public class IgniteServiceGridTestSuite {
     /** */
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteTopologyValidatorTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteTopologyValidatorTestSuite.java
index 4fddbe0..ba3e556 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteTopologyValidatorTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteTopologyValidatorTestSuite.java
@@ -20,6 +20,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import org.apache.ignite.internal.processors.cache.CacheTopologyValidatorProviderTest;
 import org.apache.ignite.internal.processors.cache.IgniteTopologyValidatorGridSplitCacheTest;
 import org.apache.ignite.internal.processors.cache.IgniteTopologyValidatorNearPartitionedAtomicCacheGroupsTest;
 import org.apache.ignite.internal.processors.cache.IgniteTopologyValidatorNearPartitionedAtomicCacheTest;
@@ -61,6 +62,7 @@
         GridTestUtils.addTestIfNeeded(suite, IgniteTopologyValidatorReplicatedTxCacheGroupsTest.class, ignoredTests);
 
         GridTestUtils.addTestIfNeeded(suite, IgniteTopologyValidatorGridSplitCacheTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, CacheTopologyValidatorProviderTest.class, ignoredTests);
 
         return suite;
     }
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java
index baa7730..5126a47 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java
@@ -28,8 +28,10 @@
 import org.apache.ignite.internal.util.IgniteDevOnlyLogTest;
 import org.apache.ignite.internal.util.IgniteExceptionRegistrySelfTest;
 import org.apache.ignite.internal.util.IgniteUtilsSelfTest;
+import org.apache.ignite.internal.util.IgniteUtilsUnitTest;
 import org.apache.ignite.internal.util.nio.GridNioDelimitedBufferSelfTest;
 import org.apache.ignite.internal.util.nio.GridNioSelfTest;
+import org.apache.ignite.internal.util.nio.GridNioServerTest;
 import org.apache.ignite.internal.util.nio.GridNioSessionMetaKeySelfTest;
 import org.apache.ignite.internal.util.nio.GridNioSslSelfTest;
 import org.apache.ignite.internal.util.nio.impl.GridNioFilterChainSelfTest;
@@ -80,6 +82,7 @@
     GridThreadPoolExecutorServiceSelfTest.class,
     IgniteThreadPoolSizeTest.class,
     IgniteUtilsSelfTest.class,
+    IgniteUtilsUnitTest.class,
     IgniteVersionUtilsSelfTest.class,
     GridSpinReadWriteLockSelfTest.class,
     GridQueueSelfTest.class,
@@ -128,6 +131,7 @@
     // NIO.
     GridNioSessionMetaKeySelfTest.class,
     GridNioSelfTest.class,
+    GridNioServerTest.class,
     GridNioFilterChainSelfTest.class,
     GridNioSslSelfTest.class,
     GridNioDelimitedBufferSelfTest.class,
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
index 390cca7..d175fcc 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
@@ -1,5 +1,5 @@
 Control utility [ver. <!any!>
-2021 Copyright(C) Apache Software Foundation
+<!copyright!>
 User: <!any!>
 Time: <!any!>
 Command [CACHE] started
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
index 6a0ca41..a5e628b 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
@@ -1,5 +1,5 @@
 Control utility [ver. <!any!>
-2021 Copyright(C) Apache Software Foundation
+<!copyright!>
 User: <!any!>
 Time: <!any!>
 Control utility script is used to execute admin commands on cluster or get common cluster info. The command has the following syntax:
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
index 390cca7..d175fcc 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
@@ -1,5 +1,5 @@
 Control utility [ver. <!any!>
-2021 Copyright(C) Apache Software Foundation
+<!copyright!>
 User: <!any!>
 Time: <!any!>
 Command [CACHE] started
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
index 6a0ca41..a5e628b 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
@@ -1,5 +1,5 @@
 Control utility [ver. <!any!>
-2021 Copyright(C) Apache Software Foundation
+<!copyright!>
 User: <!any!>
 Time: <!any!>
 Control utility script is used to execute admin commands on cluster or get common cluster info. The command has the following syntax:
diff --git a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverter.java b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverter.java
index e5a5838..c13d63f 100644
--- a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverter.java
+++ b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverter.java
@@ -194,10 +194,12 @@
         if (walRecord instanceof DataRecord) {
             final DataRecord dataRecord = (DataRecord)walRecord;
 
-            final List<DataEntry> entryWrappers = new ArrayList<>(dataRecord.writeEntries().size());
+            int entryCnt = dataRecord.entryCount();
 
-            for (DataEntry dataEntry : dataRecord.writeEntries())
-                entryWrappers.add(new DataEntryWrapper(dataEntry, sensitiveData));
+            final List<DataEntry> entryWrappers = new ArrayList<>(entryCnt);
+
+            for (int i = 0; i < entryCnt; i++)
+                entryWrappers.add(new DataEntryWrapper(dataRecord.get(i), sensitiveData));
 
             dataRecord.setWriteEntries(entryWrappers);
         }
diff --git a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverterArguments.java b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverterArguments.java
index b6f9e47..ec4c5a8 100644
--- a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverterArguments.java
+++ b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/IgniteWalConverterArguments.java
@@ -86,7 +86,7 @@
     private static final String PAGES = "pages";
 
     /** Record pattern for {@link #PAGES}. */
-    private static final Pattern PAGE_ID_PATTERN = Pattern.compile("(\\d+):(\\d+)");
+    private static final Pattern PAGE_ID_PATTERN = Pattern.compile("(-?\\d+):(-?\\d+)");
 
     /** Path to dir with wal files. */
     private final File walDir;
diff --git a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/WalStat.java b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/WalStat.java
index 84be225..93f0d1b 100644
--- a/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/WalStat.java
+++ b/modules/dev-utils/src/main/java/org/apache/ignite/development/utils/WalStat.java
@@ -205,11 +205,14 @@
      * @param record data record to handle.
      */
     private void registerDataRecord(DataRecord record) {
-        final List<DataEntry> dataEntries = record.writeEntries();
-        if (!dataEntries.isEmpty()) {
+        int entryCnt = record.entryCount();
+
+        if (entryCnt != 0) {
             boolean underTx = false;
-            for (DataEntry next : dataEntries) {
-                final int size = dataEntries.size() > 1 ? -1 : record.size();
+            for (int i = 0; i < entryCnt; i++) {
+                DataEntry next = record.get(i);
+
+                final int size = entryCnt > 1 ? -1 : record.size();
                 incrementStat(next.op().toString(), dataEntryOperation, size);
 
                 incrementStat(next.cacheId(), dataEntryCacheId, size);
@@ -222,7 +225,7 @@
             incrementStat(underTx, record, dataRecordUnderTx);
         }
 
-        incrementStat(dataEntries.size(), record, dataRecordEntriesCnt);
+        incrementStat(entryCnt, record, dataRecordEntriesCnt);
     }
 
     /**
diff --git a/modules/dev-utils/src/test/java/org/apache/ignite/development/utils/IgniteWalConverterArgumentsTest.java b/modules/dev-utils/src/test/java/org/apache/ignite/development/utils/IgniteWalConverterArgumentsTest.java
index 0b710a3b..fba6dba 100644
--- a/modules/dev-utils/src/test/java/org/apache/ignite/development/utils/IgniteWalConverterArgumentsTest.java
+++ b/modules/dev-utils/src/test/java/org/apache/ignite/development/utils/IgniteWalConverterArgumentsTest.java
@@ -445,6 +445,9 @@
             assertThrows(log, () -> parsePageId(v), IllegalArgumentException.class, null);
 
         assertEquals(new T2<>(1, 1L), parsePageId("1:1"));
+        assertEquals(new T2<>(-1, 1L), parsePageId("-1:1"));
+        assertEquals(new T2<>(1, -1L), parsePageId("1:-1"));
+        assertEquals(new T2<>(-1, -1L), parsePageId("-1:-1"));
     }
 
     /**
@@ -474,6 +477,15 @@
 
             U.writeStringToFile(f, U.nl() + "2:2", defaultCharset().toString(), true);
             assertEqualsCollections(F.asList(new T2<>(1, 1L), new T2<>(2, 2L)), parsePageIds(f));
+
+            U.writeStringToFile(f, U.nl() + "-1:1", defaultCharset().toString(), true);
+            U.writeStringToFile(f, U.nl() + "1:-1", defaultCharset().toString(), true);
+            U.writeStringToFile(f, U.nl() + "-1:-1", defaultCharset().toString(), true);
+
+            assertEqualsCollections(
+                F.asList(new T2<>(1, 1L), new T2<>(2, 2L), new T2<>(-1, 1L), new T2<>(1, -1L), new T2<>(-1, -1L)),
+                parsePageIds(f)
+            );
         }
         finally {
             assertTrue(U.delete(f));
@@ -491,7 +503,11 @@
         assertThrows(log, () -> parsePageIds("1:1", "a:b"), IllegalArgumentException.class, null);
 
         assertEqualsCollections(F.asList(new T2<>(1, 1L)), parsePageIds("1:1"));
-        assertEqualsCollections(F.asList(new T2<>(1, 1L), new T2<>(2, 2L)), parsePageIds("1:1", "2:2"));
+
+        assertEqualsCollections(
+            F.asList(new T2<>(1, 1L), new T2<>(2, 2L), new T2<>(-1, 1L), new T2<>(1, -1L), new T2<>(-1, -1L)),
+            parsePageIds("1:1", "2:2", "-1:1", "1:-1", "-1:-1")
+        );
     }
 
     /**
diff --git a/modules/ducktests/pom.xml b/modules/ducktests/pom.xml
index 8ed911e..cb63967 100644
--- a/modules/ducktests/pom.xml
+++ b/modules/ducktests/pom.xml
@@ -98,6 +98,26 @@
                     <groupId>org.apache.zookeeper</groupId>
                     <artifactId>zookeeper</artifactId>
                 </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jul-to-slf4j</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>log4j</groupId>
+                    <artifactId>log4j</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
 
@@ -166,6 +186,12 @@
             <artifactId>htrace-core4</artifactId>
             <version>4.1.0-incubating</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
index f5f9be6..c42a891 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
@@ -282,9 +282,9 @@
 
     def __jackson(self):
         if not self.service.config.version.is_dev:
-            aws = self._module("aws")
+            ducktests = self._module("ducktests")
             return self.service.context.cluster.nodes[0].account.ssh_capture(
-                "ls -d %s/* | grep jackson | tr '\n' ':' | sed 's/.$//'" % aws)
+                "find %s -type f -name '*.jar' | grep jackson | tr '\n' ':' " % ducktests)
 
         return []
 
diff --git a/modules/ducktests/tests/ignitetest/tests/control_utility/consistency_test.py b/modules/ducktests/tests/ignitetest/tests/control_utility/consistency_test.py
index 2caa273..decb80c 100644
--- a/modules/ducktests/tests/ignitetest/tests/control_utility/consistency_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/control_utility/consistency_test.py
@@ -101,7 +101,7 @@
         except AssertionError:
             pass
 
-        control_utility.check_consistency(f"repair {self.CACHE_NAME} 0")  # checking/repairing
+        control_utility.check_consistency(f"repair --cache {self.CACHE_NAME} --partition 0 --strategy LWW")  # checking/repairing
 
         message = "Cache consistency violations recorded."
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewLocal.java b/modules/indexing/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewLocal.java
index 2cf1a20..68e2b41 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewLocal.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/managers/systemview/SystemViewLocal.java
@@ -51,6 +51,7 @@
 import org.h2.value.ValueString;
 import org.h2.value.ValueTimestamp;
 import org.h2.value.ValueUuid;
+import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.toSqlName;
 
@@ -155,7 +156,7 @@
                 Value[] data = new Value[sysView.walker().count()];
 
                 sysView.walker().visitAll(row, new AttributeWithValueVisitor() {
-                    @Override public <T> void accept(int idx, String name, Class<T> clazz, T val) {
+                    @Override public <T> void accept(int idx, String name, Class<T> clazz, @Nullable T val) {
                         if (val == null)
                             data[idx] = ValueNull.INSTANCE;
                         else
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheTwoStepQuery.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheTwoStepQuery.java
index 85a50b4..ab97678 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheTwoStepQuery.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheTwoStepQuery.java
@@ -86,6 +86,7 @@
      * @param cacheIds Cache ids.
      * @param mvccEnabled Mvcc flag.
      * @param locSplit Local split flag.
+     * @param treatReplicatedAsPartitioned Treat replicated as partitioned flag.
      */
     public GridCacheTwoStepQuery(
         String originalSql,
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/RegisteredQueryCursor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/RegisteredQueryCursor.java
index 3945fec..4f00b5c 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/RegisteredQueryCursor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/cache/query/RegisteredQueryCursor.java
@@ -50,7 +50,7 @@
     private RunningQueryManager runningQryMgr;
 
     /** */
-    private Long qryId;
+    private long qryId;
 
     /** Exception caused query failed or {@code null} if it succeded. */
     private Exception failReason;
@@ -70,11 +70,11 @@
      * @param tracing Tracing processor.
      */
     public RegisteredQueryCursor(Iterable<T> iterExec, GridQueryCancel cancel, RunningQueryManager runningQryMgr,
-        boolean lazy, Long qryId, Tracing tracing) {
+        boolean lazy, long qryId, Tracing tracing) {
         super(iterExec, cancel, true, lazy);
 
         assert runningQryMgr != null;
-        assert qryId != null;
+        assert qryId != RunningQueryManager.UNDEFINED_QUERY_ID;
 
         this.runningQryMgr = runningQryMgr;
         this.qryId = qryId;
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
index 074f6a2..27691da 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
@@ -408,7 +408,7 @@
      * @return Result.
      */
     public CommandResult runCommand(String sql, SqlCommand cmdNative, GridSqlStatement cmdH2,
-        QueryParameters params, @Nullable SqlClientContext cliCtx, Long qryId) throws IgniteCheckedException {
+        QueryParameters params, @Nullable SqlClientContext cliCtx, long qryId) throws IgniteCheckedException {
         assert cmdNative != null || cmdH2 != null;
 
         // Do execute.
@@ -1419,7 +1419,7 @@
      * @return The context (which is the result of the first request/response).
      * @throws IgniteCheckedException If something failed.
      */
-    private FieldsQueryCursor<List<?>> processBulkLoadCommand(SqlBulkLoadCommand cmd, Long qryId)
+    private FieldsQueryCursor<List<?>> processBulkLoadCommand(SqlBulkLoadCommand cmd, long qryId)
         throws IgniteCheckedException {
         if (cmd.packetSize() == null)
             cmd.packetSize(BulkLoadAckClientParameters.DFLT_PACKET_SIZE);
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizer.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizer.java
index 51c6da6..26e652f 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizer.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizer.java
@@ -154,6 +154,9 @@
 
             if (col instanceof GridSqlSubquery)
                 wasPulledOut = pullOutSubQryFromSelectExpr(select, null, i);
+            else if (col instanceof GridSqlOperation && ((GridSqlOperation)col).operationType() == EXISTS)
+                //We actially can't rewrite select query under exists operator. So we skip it.
+                continue;
             else {
                 ASTNodeFinder finder = new ASTNodeFinder(
                     col,
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryFetchSizeInterceptor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryFetchSizeInterceptor.java
index cf47a7c..ebafc0c 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryFetchSizeInterceptor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryFetchSizeInterceptor.java
@@ -68,7 +68,7 @@
         ++fetchedSize;
 
         if (threshold > 0 && fetchedSize >= threshold) {
-            qryInfo.printLogMessage(log, "Query produced big result set. ",
+            qryInfo.printLogMessage(log, "Query produced big result set.",
                 "fetched=" + fetchedSize);
 
             if (thresholdMult > 1)
@@ -85,7 +85,7 @@
      */
     public void checkOnClose() {
         if (bigResults) {
-            qryInfo.printLogMessage(log, "Query produced big result set. ",
+            qryInfo.printLogMessage(log, "Query produced big result set.",
                 "fetched=" + fetchedSize);
         }
     }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryInfo.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryInfo.java
index 2a62fc9..09afcbb 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryInfo.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2QueryInfo.java
@@ -19,9 +19,12 @@
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
+import java.util.UUID;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.RunningQueryManager;
 import org.apache.ignite.internal.processors.query.h2.sql.GridSqlQueryParser;
 import org.apache.ignite.internal.util.typedef.internal.LT;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -56,17 +59,27 @@
     /** Prepared statement. */
     private final Prepared stmt;
 
+    /** Originator node uid. */
+    private final UUID nodeId;
+
+    /** Query id. */
+    private final long queryId;
+
     /**
      * @param type Query type.
      * @param stmt Query statement.
      * @param sql Query statement.
+     * @param nodeId Originator node id.
+     * @param queryId Query id.
      */
-    public H2QueryInfo(QueryType type, PreparedStatement stmt, String sql) {
+    public H2QueryInfo(QueryType type, PreparedStatement stmt, String sql, UUID nodeId, long queryId) {
         try {
             assert stmt != null;
 
             this.type = type;
             this.sql = sql;
+            this.nodeId = nodeId;
+            this.queryId = queryId;
 
             beginTs = U.currentTimeMillis();
 
@@ -106,22 +119,24 @@
      * @param additionalInfo Additional query info.
      */
     public void printLogMessage(IgniteLogger log, String msg, String additionalInfo) {
-        StringBuilder msgSb = new StringBuilder(msg + " [");
+        StringBuilder msgSb = new StringBuilder(msg);
+
+        if (queryId == RunningQueryManager.UNDEFINED_QUERY_ID)
+            msgSb.append(" [globalQueryId=(undefined), node=").append(nodeId);
+        else
+            msgSb.append(" [globalQueryId=").append(QueryUtils.globalQueryId(nodeId, queryId));
 
         if (additionalInfo != null)
-            msgSb.append(additionalInfo).append(", ");
+            msgSb.append(", ").append(additionalInfo);
 
-        msgSb.append("duration=").append(time()).append("ms")
-            .append(", type=").append(type)
-            .append(", distributedJoin=").append(distributedJoin)
-            .append(", enforceJoinOrder=").append(enforceJoinOrder)
-            .append(", lazy=").append(lazy)
-            .append(", schema=").append(schema);
-
-        msgSb.append(", sql='")
-            .append(sql);
-
-        msgSb.append("', plan=").append(stmt.getPlanSQL());
+        msgSb.append(", duration=").append(time()).append("ms")
+                .append(", type=").append(type)
+                .append(", distributedJoin=").append(distributedJoin)
+                .append(", enforceJoinOrder=").append(enforceJoinOrder)
+                .append(", lazy=").append(lazy)
+                .append(", schema=").append(schema)
+                .append(", sql='").append(sql)
+                .append("', plan=").append(stmt.getPlanSQL());
 
         printInfo(msgSb);
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2Utils.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2Utils.java
index 121b16a..2737bd3 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2Utils.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2Utils.java
@@ -46,6 +46,8 @@
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.binary.BinaryArray;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.CacheObjectValueContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
@@ -635,7 +637,7 @@
             case Value.JAVA_OBJECT:
                 return ValueJavaObject.getNoCopy(obj, null, null);
             case Value.ARRAY:
-                Object[] arr = (Object[])obj;
+                Object[] arr = BinaryUtils.rawArrayFromBinary(obj);
 
                 Value[] valArr = new Value[arr.length];
 
@@ -837,6 +839,8 @@
                 stmt.setObject(idx, obj, Types.JAVA_OBJECT);
             else if (obj instanceof BigDecimal)
                 stmt.setObject(idx, obj, Types.DECIMAL);
+            else if (obj instanceof BinaryArray)
+                stmt.setObject(idx, BinaryUtils.rawArrayFromBinary(obj));
             else
                 stmt.setObject(idx, obj);
         }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
index f8c79f1..9b13d26 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
@@ -55,6 +55,8 @@
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.GridTopic;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.binary.BinaryMarshaller;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.cache.query.index.IndexName;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypes;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
@@ -512,7 +514,7 @@
         H2TableDescriptor tbl = schemaMgr.tableForType(schemaName, cacheName, typeName);
 
         if (tbl != null && tbl.luceneIndex() != null) {
-            Long qryId = runningQueryManager().register(qry, TEXT, schemaName, true, null, null);
+            long qryId = runningQueryManager().register(qry, TEXT, schemaName, true, null, null);
 
             try {
                 return tbl.luceneIndex().query(qry.toUpperCase(), filters, limit);
@@ -528,6 +530,7 @@
     /**
      * Queries individual fields (generally used by JDBC drivers).
      *
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param select Select.
@@ -540,6 +543,7 @@
      * @throws IgniteCheckedException If failed.
      */
     private GridQueryFieldsResult executeSelectLocal(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultSelect select,
@@ -588,13 +592,26 @@
                         H2Utils.setupConnection(conn, qctx,
                             qryDesc.distributedJoins(), qryDesc.enforceJoinOrder(), qryParams.lazy());
 
-                        List<Object> args = F.asList(qryParams.arguments());
-
                         PreparedStatement stmt = conn.prepareStatement(qry, H2StatementCache.queryFlags(qryDesc));
 
-                        H2Utils.bindParameters(stmt, args);
+                        // Convert parameters into BinaryObjects.
+                        Marshaller m = ctx.config().getMarshaller();
+                        byte[] paramsBytes = U.marshal(m, qryParams.arguments());
+                        final ClassLoader ldr = U.resolveClassLoader(ctx.config());
 
-                        H2QueryInfo qryInfo = new H2QueryInfo(H2QueryInfo.QueryType.LOCAL, stmt, qry);
+                        Object[] params;
+
+                        if (m instanceof BinaryMarshaller) {
+                            params = BinaryUtils.rawArrayFromBinary(((BinaryMarshaller)m).binaryMarshaller()
+                                .unmarshal(paramsBytes, ldr));
+                        }
+                        else
+                            params = U.unmarshal(m, paramsBytes, ldr);
+
+                        H2Utils.bindParameters(stmt, F.asList(params));
+
+                        H2QueryInfo qryInfo = new H2QueryInfo(H2QueryInfo.QueryType.LOCAL, stmt, qry,
+                            ctx.localNodeId(), qryId);
 
                         ResultSet rs = executeSqlQueryWithTimer(
                             stmt,
@@ -717,7 +734,7 @@
     @SuppressWarnings({"unchecked"})
     private long streamQuery0(String qry, String schemaName, IgniteDataStreamer streamer, QueryParserResultDml dml,
         final Object[] args, String qryInitiatorId) throws IgniteCheckedException {
-        Long qryId = runningQryMgr.register(
+        long qryId = runningQryMgr.register(
             QueryUtils.INCLUDE_SENSITIVE ? qry : sqlWithoutConst(dml.statement()),
             GridCacheQueryType.SQL_FIELDS,
             schemaName,
@@ -731,7 +748,7 @@
         try {
             UpdatePlan plan = dml.plan();
 
-            Iterator<List<?>> iter = new GridQueryCacheObjectsIterator(updateQueryRows(schemaName, plan, args),
+            Iterator<List<?>> iter = new GridQueryCacheObjectsIterator(updateQueryRows(qryId, schemaName, plan, args),
                 objectContext(), true);
 
             if (!iter.hasNext())
@@ -775,13 +792,15 @@
     /**
      * Calculates rows for update query.
      *
+     * @param qryId Query id.
      * @param schemaName Schema name.
      * @param plan Update plan.
      * @param args Statement arguments.
      * @return Rows for update.
      * @throws IgniteCheckedException If failed.
      */
-    private Iterator<List<?>> updateQueryRows(String schemaName, UpdatePlan plan, Object[] args) throws IgniteCheckedException {
+    private Iterator<List<?>> updateQueryRows(long qryId, String schemaName, UpdatePlan plan, Object[] args)
+        throws IgniteCheckedException {
         Object[] params = args != null ? args : X.EMPTY_OBJECT_ARRAY;
 
         if (!F.isEmpty(plan.selectQuery())) {
@@ -792,6 +811,7 @@
             QueryParserResult selectParseRes = parser.parse(schemaName, selectQry, false);
 
             GridQueryFieldsResult res = executeSelectLocal(
+                qryId,
                 selectParseRes.queryDescriptor(),
                 selectParseRes.queryParameters(),
                 selectParseRes.select(),
@@ -1043,7 +1063,7 @@
                 IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
         }
 
-        Long qryId = registerRunningQuery(qryDesc, qryParams, null, null);
+        long qryId = registerRunningQuery(qryDesc, qryParams, null, null);
 
         CommandResult res = null;
 
@@ -1224,7 +1244,7 @@
     ) {
         IndexingQueryFilter filter = (qryDesc.local() ? backupFilter(null, qryParams.partitions()) : null);
 
-        Long qryId = registerRunningQuery(qryDesc, qryParams, cancel, dml.statement());
+        long qryId = registerRunningQuery(qryDesc, qryParams, cancel, dml.statement());
 
         Exception failReason = null;
 
@@ -1238,6 +1258,7 @@
 
             if (!qryDesc.local()) {
                 return executeUpdateDistributed(
+                    qryId,
                     qryDesc,
                     qryParams,
                     dml,
@@ -1246,6 +1267,7 @@
             }
             else {
                 UpdateResult updRes = executeUpdate(
+                    qryId,
                     qryDesc,
                     qryParams,
                     dml,
@@ -1308,7 +1330,7 @@
         assert cancel != null;
 
         // Register query.
-        Long qryId = registerRunningQuery(qryDesc, qryParams, cancel, select.statement());
+        long qryId = registerRunningQuery(qryDesc, qryParams, cancel, select.statement());
 
         try (TraceSurroundings ignored = MTC.support(ctx.tracing().create(SQL_CURSOR_OPEN, MTC.span()))) {
             GridNearTxLocal tx = null;
@@ -1342,6 +1364,7 @@
             int timeout = operationTimeout(qryParams.timeout(), tx);
 
             Iterable<List<?>> iter = executeSelect0(
+                qryId,
                 qryDesc,
                 qryParams,
                 select,
@@ -1382,6 +1405,7 @@
     /**
      * Execute SELECT statement for DML.
      *
+     * @param qryId Query id.
      * @param schema Schema.
      * @param selectQry Select query.
      * @param mvccTracker MVCC tracker.
@@ -1391,6 +1415,7 @@
      * @throws IgniteCheckedException On error.
      */
     private QueryCursorImpl<List<?>> executeSelectForDml(
+        long qryId,
         String schema,
         SqlFieldsQuery selectQry,
         MvccQueryTracker mvccTracker,
@@ -1404,6 +1429,7 @@
         assert select != null;
 
         Iterable<List<?>> iter = executeSelect0(
+            qryId,
             parseRes.queryDescriptor(),
             parseRes.queryParameters(),
             select,
@@ -1426,6 +1452,7 @@
     /**
      * Execute an all-ready {@link SqlFieldsQuery}.
      *
+     * @param qryId Query id.
      * @param qryDesc Plan key.
      * @param qryParams Parameters.
      * @param select Select.
@@ -1438,6 +1465,7 @@
      * @throws IgniteCheckedException On error.
      */
     private Iterable<List<?>> executeSelect0(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultSelect select,
@@ -1463,6 +1491,7 @@
             assert twoStepQry != null;
 
             iter = executeSelectDistributed(
+                qryId,
                 qryDesc,
                 qryParams,
                 twoStepQry,
@@ -1477,6 +1506,7 @@
             IndexingQueryFilter filter = (qryDesc.local() ? backupFilter(null, qryParams.partitions()) : null);
 
             GridQueryFieldsResult res = executeSelectLocal(
+                qryId,
                 qryDesc,
                 qryParams,
                 select,
@@ -1572,7 +1602,7 @@
      * @param stmnt Parsed statement.
      * @return Id of registered query or {@code null} if query wasn't registered.
      */
-    private Long registerRunningQuery(
+    private long registerRunningQuery(
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         GridQueryCancel cancel,
@@ -1580,7 +1610,7 @@
     ) {
         String qry = QueryUtils.INCLUDE_SENSITIVE || stmnt == null ? qryDesc.sql() : sqlWithoutConst(stmnt);
 
-        Long res = runningQryMgr.register(
+        long res = runningQryMgr.register(
             qry,
             GridCacheQueryType.SQL_FIELDS,
             qryDesc.schemaName(),
@@ -1712,6 +1742,7 @@
         // sub-query and not some dummy stuff like "select 1, 2, 3;"
         if (!loc && !plan.isLocalSubquery()) {
             cur = executeSelectForDml(
+                RunningQueryManager.UNDEFINED_QUERY_ID,
                 schema,
                 selectFieldsQry,
                 new StaticMvccQueryTracker(planCctx, mvccSnapshot),
@@ -1725,6 +1756,7 @@
             QueryParserResult selectParseRes = parser.parse(schema, selectFieldsQry, false);
 
             GridQueryFieldsResult res = executeSelectLocal(
+                RunningQueryManager.UNDEFINED_QUERY_ID,
                 selectParseRes.queryDescriptor(),
                 selectParseRes.queryParameters(),
                 selectParseRes.select(),
@@ -1753,6 +1785,7 @@
     /**
      * Run distributed query on detected set of partitions.
      *
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param twoStepQry Two-step query.
@@ -1764,6 +1797,7 @@
      */
     @SuppressWarnings("IfMayBeConditional")
     private Iterable<List<?>> executeSelectDistributed(
+        final long qryId,
         final QueryDescriptor qryDesc,
         final QueryParameters qryParams,
         final GridCacheTwoStepQuery twoStepQry,
@@ -1775,7 +1809,7 @@
         // When explicit partitions are set, there must be an owning cache they should be applied to.
         PartitionResult derivedParts = twoStepQry.derivedPartitions();
 
-        final int parts[] = PartitionResult.calculatePartitions(
+        final int[] parts = PartitionResult.calculatePartitions(
             qryParams.partitions(),
             derivedParts,
             qryParams.arguments()
@@ -1807,6 +1841,7 @@
                 @Override public Iterator<List<?>> iterator() {
                     try (TraceSurroundings ignored = MTC.support(ctx.tracing().create(SQL_ITER_OPEN, MTC.span()))) {
                         return IgniteH2Indexing.this.rdcQryExec.query(
+                            qryId,
                             qryDesc.schemaName(),
                             twoStepQry,
                             keepBinary,
@@ -1861,6 +1896,7 @@
         assert dml != null;
 
         return executeUpdate(
+            RunningQueryManager.UNDEFINED_QUERY_ID,
             parseRes.queryDescriptor(),
             parseRes.queryParameters(),
             dml,
@@ -2085,7 +2121,7 @@
     }
 
     /** {@inheritDoc} */
-    @SuppressWarnings({"deprecation"})
+    @SuppressWarnings({"deprecation", "AssignmentToStaticFieldFromInstanceMethod"})
     @Override public void start(GridKernalContext ctx, GridSpinBusyLock busyLock) throws IgniteCheckedException {
         if (log.isDebugEnabled())
             log.debug("Starting cache query index...");
@@ -2535,6 +2571,7 @@
     }
 
     /**
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param dml DML statement.
@@ -2544,6 +2581,7 @@
      */
     @SuppressWarnings("unchecked")
     private List<QueryCursorImpl<List<?>>> executeUpdateDistributed(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultDml dml,
@@ -2587,6 +2625,7 @@
 
                     try {
                         res = executeUpdate(
+                            qryId,
                             qryDesc,
                             qryParams.toSingleBatchedArguments(args),
                             dml,
@@ -2633,6 +2672,7 @@
         }
         else {
             UpdateResult res = executeUpdate(
+                qryId,
                 qryDesc,
                 qryParams,
                 dml,
@@ -2657,6 +2697,7 @@
     /**
      * Execute DML statement, possibly with few re-attempts in case of concurrent data modifications.
      *
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param dml DML command.
@@ -2668,6 +2709,7 @@
      */
     @SuppressWarnings("IfMayBeConditional")
     private UpdateResult executeUpdate(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultDml dml,
@@ -2695,6 +2737,7 @@
             try {
                 if (transactional)
                     r = executeUpdateTransactional(
+                        qryId,
                         qryDesc,
                         qryParams,
                         dml,
@@ -2703,6 +2746,7 @@
                     );
                 else
                     r = executeUpdateNonTransactional(
+                        qryId,
                         qryDesc,
                         qryParams,
                         dml,
@@ -2737,6 +2781,7 @@
     /**
      * Execute update in non-transactional mode.
      *
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param dml Plan.
@@ -2747,6 +2792,7 @@
      * @throws IgniteCheckedException If failed.
      */
     private UpdateResult executeUpdateNonTransactional(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultDml dml,
@@ -2811,6 +2857,7 @@
             assert !F.isEmpty(plan.selectQuery());
 
             cur = executeSelectForDml(
+                qryId,
                 qryDesc.schemaName(),
                 selectFieldsQry,
                 null,
@@ -2826,6 +2873,7 @@
             QueryParserResult selectParseRes = parser.parse(qryDesc.schemaName(), selectFieldsQry, false);
 
             final GridQueryFieldsResult res = executeSelectLocal(
+                qryId,
                 selectParseRes.queryDescriptor(),
                 selectParseRes.queryParameters(),
                 selectParseRes.select(),
@@ -2863,6 +2911,7 @@
     /**
      * Execute update in transactional mode.
      *
+     * @param qryId Query id.
      * @param qryDesc Query descriptor.
      * @param qryParams Query parameters.
      * @param dml Plan.
@@ -2872,6 +2921,7 @@
      * @throws IgniteCheckedException If failed.
      */
     private UpdateResult executeUpdateTransactional(
+        long qryId,
         QueryDescriptor qryDesc,
         QueryParameters qryParams,
         QueryParserResultDml dml,
@@ -2939,6 +2989,7 @@
                         .setLazy(qryParams.lazy());
 
                     FieldsQueryCursor<List<?>> cur = executeSelectForDml(
+                        qryId,
                         qryDesc.schemaName(),
                         selectFieldsQry,
                         MvccUtils.mvccTracker(cctx, tx),
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/MapH2QueryInfo.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/MapH2QueryInfo.java
index 5b8db67..5b1cccd 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/MapH2QueryInfo.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/MapH2QueryInfo.java
@@ -18,15 +18,12 @@
 package org.apache.ignite.internal.processors.query.h2;
 
 import java.sql.PreparedStatement;
-import org.apache.ignite.cluster.ClusterNode;
+import java.util.UUID;
 
 /**
  * Map query info.
  */
 public class MapH2QueryInfo extends H2QueryInfo {
-    /** Node. */
-    private final ClusterNode node;
-
     /** Request id. */
     private final long reqId;
 
@@ -36,23 +33,22 @@
     /**
      * @param stmt Query statement.
      * @param sql Query statement.
-     * @param node Originator node ID
+     * @param nodeId Originator node id.
+     * @param qryId Query id.
      * @param reqId Request ID.
      * @param segment Segment.
      */
-    public MapH2QueryInfo(PreparedStatement stmt, String sql,
-        ClusterNode node, long reqId, int segment) {
-        super(QueryType.MAP, stmt, sql);
+    public MapH2QueryInfo(PreparedStatement stmt, String sql, UUID nodeId, long qryId, long reqId,
+        int segment) {
+        super(QueryType.MAP, stmt, sql, nodeId, qryId);
 
-        this.node = node;
         this.reqId = reqId;
         this.segment = segment;
     }
 
     /** {@inheritDoc} */
     @Override protected void printInfo(StringBuilder msg) {
-        msg.append(", node=").append(node)
-            .append(", reqId=").append(reqId)
+        msg.append(", reqId=").append(reqId)
             .append(", segment=").append(segment);
     }
 }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ReduceH2QueryInfo.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ReduceH2QueryInfo.java
index 9474c6a..ea88594 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ReduceH2QueryInfo.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ReduceH2QueryInfo.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.query.h2;
 
 import java.sql.PreparedStatement;
+import java.util.UUID;
 
 /**
  * Reduce query info.
@@ -29,10 +30,12 @@
     /**
      * @param stmt Query statement.
      * @param sql Query statement.
+     * @param nodeId Originator node id.
+     * @param qryId Query id.
      * @param reqId Request ID.
      */
-    public ReduceH2QueryInfo(PreparedStatement stmt, String sql, long reqId) {
-        super(QueryType.REDUCE, stmt, sql);
+    public ReduceH2QueryInfo(PreparedStatement stmt, String sql, UUID nodeId, long qryId, long reqId) {
+        super(QueryType.REDUCE, stmt, sql, nodeId, qryId);
 
         this.reqId = reqId;
     }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
index 18baad6..74a1a83 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
@@ -204,7 +204,7 @@
 
             QueryContext qctx = ses != null ? H2Utils.context(ses) : null;
 
-            GridCursor<IndexRow> cursor = queryIndex.find(key.get1(), key.get2(), segment(qctx), idxQryContext(qctx));
+            GridCursor<IndexRow> cursor = queryIndex.find(key.get1(), key.get2(), true, true, segment(qctx), idxQryContext(qctx));
 
             GridCursor<H2Row> h2cursor = new IndexValueCursor<>(cursor, this::mapIndexRow);
 
@@ -222,7 +222,7 @@
         if (qctx == null)
             return null;
 
-        return new IndexQueryContext(qctx.filter(), qctx.mvccSnapshot());
+        return new IndexQueryContext(qctx.filter(), null, qctx.mvccSnapshot());
     }
 
     /** */
@@ -579,7 +579,7 @@
         T2<IndexRow, IndexRow> key = prepareIndexKeys(lower, upper);
 
         try {
-            GridCursor<IndexRow> range = queryIndex.find(key.get1(), key.get2(), segment, qryCtx);
+            GridCursor<IndexRow> range = queryIndex.find(key.get1(), key.get2(), true, true, segment, qryCtx);
 
             if (range == null)
                 range = IndexValueCursor.EMPTY;
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/dml/DmlUtils.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/dml/DmlUtils.java
index f397d60..2b6a89b 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/dml/DmlUtils.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/dml/DmlUtils.java
@@ -33,6 +33,7 @@
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.processors.cache.CacheOperationContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.QueryCursorImpl;
@@ -111,6 +112,9 @@
             // We have to convert arrays of reference types manually -
             // see https://issues.apache.org/jira/browse/IGNITE-4327
             // Still, we only can convert from Object[] to something more precise.
+            if (type == Value.ARRAY && val instanceof BinaryArray)
+                return val;
+
             if (type == Value.ARRAY && currCls != expCls) {
                 if (currCls != Object[].class) {
                     throw new IgniteCheckedException("Unexpected array type - only conversion from Object[] " +
@@ -555,7 +559,7 @@
 
             if (opCtx == null)
                 // Mimics behavior of GridCacheAdapter#keepBinary and GridCacheProxyImpl#keepBinary
-                newOpCtx = new CacheOperationContext(false, true, null, false, null, false, false, true);
+                newOpCtx = new CacheOperationContext(false, true, null, false, null, false, null, true);
             else if (!opCtx.isKeepBinary())
                 newOpCtx = opCtx.keepBinary();
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
index e9bb762..d0906e5 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
@@ -50,31 +50,29 @@
     /** Ignite H2 session. */
     private final SessionInterface ses;
 
-    /** Key type settings for this index. */
-    private final IndexKeyTypeSettings keyTypeSettings;
-
     /** */
     public H2RowComparator(GridH2Table table, IndexKeyTypeSettings keyTypeSettings) {
+        super(keyTypeSettings);
+
         this.table = table;
-        this.keyTypeSettings = keyTypeSettings;
 
         coctx = table.rowDescriptor().context().cacheObjectContext();
         ses = table.rowDescriptor().indexing().connections().jdbcConnection().getSession();
     }
 
     /** {@inheritDoc} */
-    @Override public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, int curType) {
-        int cmp = super.compareKey(pageAddr, off, maxSize, key, curType);
+    @Override public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, InlineIndexKeyType type) {
+        int cmp = super.compareKey(pageAddr, off, maxSize, key, type);
 
         if (cmp != COMPARE_UNSUPPORTED)
             return cmp;
 
-        int objType = key == NullIndexKey.INSTANCE ? curType : key.type();
+        int objType = key == NullIndexKey.INSTANCE ? type.type() : key.type();
 
-        int highOrder = Value.getHigherOrder(curType, objType);
+        int highOrder = Value.getHigherOrder(type.type(), objType);
 
         // H2 supports comparison between different types after casting them to single type.
-        if (highOrder != objType && highOrder == curType) {
+        if (highOrder != objType && highOrder == type.type()) {
             Value va = DataType.convertToValue(ses, key.key(), highOrder);
             va = va.convertTo(highOrder);
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/client/ClientInlineIndex.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/client/ClientInlineIndex.java
index d1f0154..14ff58a 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/client/ClientInlineIndex.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/client/ClientInlineIndex.java
@@ -31,7 +31,7 @@
 import org.jetbrains.annotations.Nullable;
 
 /**
- * We need indexes on an not affinity nodes. The index shouldn't contains any data.
+ * We need indexes on non-affinity nodes. This index does not contain any data.
  */
 public class ClientInlineIndex implements InlineIndex {
     /** */
@@ -65,13 +65,14 @@
     }
 
     /** {@inheritDoc} */
-    @Override public GridCursor<IndexRow> find(IndexRow lower, IndexRow upper, int segment) throws IgniteCheckedException {
-        throw unsupported();
-    }
-
-    /** {@inheritDoc} */
-    @Override public GridCursor<IndexRow> find(IndexRow lower, IndexRow upper, int segment,
-        IndexQueryContext qryCtx) throws IgniteCheckedException {
+    @Override public GridCursor<IndexRow> find(
+        IndexRow lower,
+        IndexRow upper,
+        boolean lowIncl,
+        boolean upIncl,
+        int segment,
+        IndexQueryContext qryCtx
+    ) throws IgniteCheckedException {
         throw unsupported();
     }
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java
index f0e3f14..5f253ab 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java
@@ -320,7 +320,7 @@
         List<Integer> cacheIds = H2Utils.collectCacheIds(idx, null, splitter.tbls);
         boolean mvccEnabled = H2Utils.collectMvccEnabled(idx, cacheIds);
         boolean replicatedOnly = splitter.mapSqlQrys.stream().noneMatch(GridCacheSqlQuery::isPartitioned);
-        boolean treatReplicatedAsPartitioned = splitter.mapSqlQrys.stream().anyMatch(GridCacheSqlQuery::hasOuterJoinReplicatedPartitioned);
+        boolean treatReplicatedAsPartitioned = splitter.mapSqlQrys.stream().anyMatch(GridCacheSqlQuery::treatReplicatedAsPartitioned);
 
         H2Utils.checkQuery(idx, cacheIds, splitter.tbls);
 
@@ -1280,7 +1280,9 @@
         map.sortColumns(mapQry.sort());
         map.partitioned(traverser.hasPartitionedTables());
         map.hasSubQueries(traverser.hasSubQueries());
-        map.hasOuterJoinReplicatedPartitioned(traverser.hasOuterJoinReplicatedPartitioned());
+        map.treatReplicatedAsPartitioned(
+            traverser.hasOuterJoinReplicatedPartitioned() || traverser.hasReplicatedWithPartitionedAndSubQuery()
+        );
 
         if (map.isPartitioned() && canExtractPartitions)
             map.derivedPartitions(extractor.extract(mapQry));
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/SqlAstTraverser.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/SqlAstTraverser.java
index 08feecd..f854c3f 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/SqlAstTraverser.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/SqlAstTraverser.java
@@ -44,6 +44,9 @@
     /** Whether query has joins between replicated and partitioned tables. */
     private boolean hasOuterJoinReplicatedPartitioned;
 
+    /** Whether top-level table is replicated. */
+    private boolean isRootTableReplicated;
+
     /** */
     SqlAstTraverser(GridSqlAst root, boolean distributedJoins, IgniteLogger log) {
         this.root = root;
@@ -53,6 +56,13 @@
 
     /** */
     public void traverse() {
+        if (root instanceof GridSqlSelect) {
+            GridSqlTable table = getTable(((GridSqlSelect)root).from().child());
+
+            if (table != null && !table.dataTable().isPartitioned())
+                isRootTableReplicated = true;
+        }
+
         lookForPartitionedJoin(root, null);
     }
 
@@ -71,6 +81,11 @@
         return hasOuterJoinReplicatedPartitioned;
     }
 
+    /** */
+    public boolean hasReplicatedWithPartitionedAndSubQuery() {
+        return (isRootTableReplicated && hasSubQueries && hasPartitionedTables);
+    }
+
     /**
      * Traverse AST while join operation isn't found. Check it if found.
      *
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridMapQueryExecutor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridMapQueryExecutor.java
index ccf967b..3144e9e 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridMapQueryExecutor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridMapQueryExecutor.java
@@ -73,7 +73,6 @@
 import org.apache.ignite.internal.processors.tracing.MTC.TraceSurroundings;
 import org.apache.ignite.internal.processors.tracing.Span;
 import org.apache.ignite.internal.processors.tracing.SpanType;
-import org.apache.ignite.internal.util.lang.GridPlainCallable;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.CU;
@@ -242,10 +241,11 @@
 
                 Span span = MTC.span();
 
-                ctx.closure().callLocal(
-                    (GridPlainCallable<Void>)() -> {
+                ctx.closure().runLocal(
+                    () -> {
                         try (TraceSurroundings ignored = MTC.supportContinual(span)) {
                             onQueryRequest0(node,
+                                req.queryId(),
                                 req.requestId(),
                                 segment,
                                 req.schemaName(),
@@ -265,14 +265,16 @@
                                 dataPageScanEnabled,
                                 treatReplicatedAsPartitioned
                             );
-
-                            return null;
+                        }
+                        catch (Throwable e) {
+                            sendError(node, req.requestId(), e);
                         }
                     },
                     QUERY_POOL);
             }
 
             onQueryRequest0(node,
+                req.queryId(),
                 req.requestId(),
                 singleSegment,
                 req.schemaName(),
@@ -300,6 +302,7 @@
 
     /**
      * @param node Node authored request.
+     * @param qryId Query ID.
      * @param reqId Request ID.
      * @param segmentId index segment ID.
      * @param schemaName Schema name.
@@ -320,6 +323,7 @@
      */
     private void onQueryRequest0(
         final ClusterNode node,
+        final long qryId,
         final long reqId,
         final int segmentId,
         final String schemaName,
@@ -454,7 +458,7 @@
 
                         H2Utils.bindParameters(stmt, params0);
 
-                        MapH2QueryInfo qryInfo = new MapH2QueryInfo(stmt, qry.query(), node, reqId, segmentId);
+                        MapH2QueryInfo qryInfo = new MapH2QueryInfo(stmt, qry.query(), node.id(), qryId, reqId, segmentId);
 
                         ResultSet rs = h2.executeSqlQueryWithTimer(
                             stmt,
@@ -562,12 +566,15 @@
                         if (qryRetryErr != null)
                             sendError(node, reqId, qryRetryErr);
                         else {
-                            U.error(log, "Failed to execute local query.", e);
+                            if (e instanceof Error) {
+                                U.error(log, "Failed to execute local query.", e);
+
+                                throw (Error)e;
+                            }
+
+                            U.warn(log, "Failed to execute local query.", e);
 
                             sendError(node, reqId, e);
-
-                            if (e instanceof Error)
-                                throw (Error)e;
                         }
                     }
                 }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridReduceQueryExecutor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridReduceQueryExecutor.java
index 2564196..a2b5a96 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridReduceQueryExecutor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/GridReduceQueryExecutor.java
@@ -130,7 +130,7 @@
     private IgniteLogger log;
 
     /** */
-    private final AtomicLong qryIdGen = new AtomicLong();
+    private final AtomicLong qryReqIdGen = new AtomicLong();
 
     /** */
     private final ConcurrentMap<Long, ReduceQueryRun> runs = new ConcurrentHashMap<>();
@@ -320,6 +320,7 @@
     }
 
     /**
+     * @param qryId Query ID.
      * @param schemaName Schema name.
      * @param qry Query.
      * @param keepBinary Keep binary.
@@ -334,8 +335,9 @@
      * @param pageSize Page size.
      * @return Rows iterator.
      */
-    @SuppressWarnings({"BusyWait", "IfMayBeConditional"})
+    @SuppressWarnings("IfMayBeConditional")
     public Iterator<List<?>> query(
+        long qryId,
         String schemaName,
         final GridCacheTwoStepQuery qry,
         boolean keepBinary,
@@ -417,11 +419,10 @@
 
             H2PooledConnection conn = h2.connections().connection(schemaName);
 
-            final long qryReqId = qryIdGen.incrementAndGet();
+            final long qryReqId = qryReqIdGen.incrementAndGet();
 
             h2.runningQueryManager().trackRequestId(qryReqId);
 
-            boolean retry = false;
             boolean release = true;
 
             try {
@@ -434,6 +435,7 @@
                     cancel.add(() -> send(nodes, new GridQueryCancelRequest(qryReqId), null, true));
 
                     GridH2QueryRequest req = new GridH2QueryRequest()
+                        .queryId(qryId)
                         .requestId(qryReqId)
                         .topologyVersion(topVer)
                         .pageSize(pageSize)
@@ -453,6 +455,8 @@
                     final C2<ClusterNode, Message, Message> spec =
                         parts == null ? null : new ReducePartitionsSpecializer(mapping.queryPartitionsMap());
 
+                    boolean retry = false;
+
                     if (send(nodes, req, spec, false)) {
                         awaitAllReplies(r, nodes, cancel);
 
@@ -462,97 +466,95 @@
                             if (err != null) {
                                 if (err.getCause() instanceof IgniteClientDisconnectedException)
                                     throw err;
-
-                                if (QueryUtils.wasCancelled(err))
+                                else if (QueryUtils.wasCancelled(err))
                                     throw new QueryCancelledException(); // Throw correct exception.
 
                                 throw err;
                             }
-                            else {
-                                retry = true;
 
-                                // If remote node asks us to retry then we have outdated full partition map.
-                                h2.awaitForReadyTopologyVersion(r.retryTopologyVersion());
-                            }
+                            // If remote node asks us to retry then we have outdated full partition map.
+                            h2.awaitForReadyTopologyVersion(r.retryTopologyVersion());
+
+                            retry = true;
                         }
                     }
-                    else // Send failed.
+                    else
                         retry = true;
 
                     if (retry) {
-                        lastRun = runs.remove(qryReqId);
+                        lastRun = runs.get(qryReqId);
+
                         assert lastRun != null;
 
-                        continue;
+                        continue; // Retry.
+                    }
+
+                    Iterator<List<?>> resIter;
+
+                    if (skipMergeTbl) {
+                        resIter = new ReduceIndexIterator(this,
+                            nodes,
+                            r,
+                            qryReqId,
+                            qry.distributedJoins(),
+                            mvccTracker,
+                            ctx.tracing());
+
+                        release = false;
+
+                        U.close(conn, log);
                     }
                     else {
-                        Iterator<List<?>> resIter;
+                        ensureQueryNotCancelled(cancel);
 
-                        if (skipMergeTbl) {
-                            resIter = new ReduceIndexIterator(this,
-                                nodes,
-                                r,
-                                qryReqId,
-                                qry.distributedJoins(),
-                                mvccTracker,
-                                ctx.tracing());
+                        QueryContext qctx = new QueryContext(
+                            0,
+                            null,
+                            null,
+                            null,
+                            null,
+                            true);
 
-                            release = false;
+                        H2Utils.setupConnection(conn, qctx, false, enforceJoinOrder);
 
-                            U.close(conn, log);
-                        }
-                        else {
-                            ensureQueryNotCancelled(cancel);
+                        if (qry.explain())
+                            return explainPlan(conn, qry, params);
 
-                            QueryContext qctx = new QueryContext(
-                                0,
-                                null,
-                                null,
-                                null,
-                                null,
-                                true
-                            );
+                        GridCacheSqlQuery rdc = qry.reduceQuery();
 
-                            H2Utils.setupConnection(conn, qctx, false, enforceJoinOrder);
+                        final PreparedStatement stmt = conn.prepareStatementNoCache(rdc.query());
 
-                            if (qry.explain())
-                                return explainPlan(conn, qry, params);
+                        H2Utils.bindParameters(stmt, F.asList(rdc.parameters(params)));
 
-                            GridCacheSqlQuery rdc = qry.reduceQuery();
+                        ReduceH2QueryInfo qryInfo = new ReduceH2QueryInfo(stmt, qry.originalSql(),
+                            ctx.localNodeId(), qryId, qryReqId);
 
-                            final PreparedStatement stmt = conn.prepareStatementNoCache(rdc.query());
+                        ResultSet res = h2.executeSqlQueryWithTimer(stmt,
+                            conn,
+                            rdc.query(),
+                            timeoutMillis,
+                            cancel,
+                            dataPageScanEnabled,
+                            qryInfo
+                        );
 
-                            H2Utils.bindParameters(stmt, F.asList(rdc.parameters(params)));
+                        resIter = new H2FieldsIterator(
+                            res,
+                            mvccTracker,
+                            conn,
+                            r.pageSize(),
+                            log,
+                            h2,
+                            qryInfo,
+                            ctx.tracing()
+                        );
 
-                            ReduceH2QueryInfo qryInfo = new ReduceH2QueryInfo(stmt, qry.originalSql(), qryReqId);
+                        conn = null;
 
-                            ResultSet res = h2.executeSqlQueryWithTimer(stmt,
-                                conn,
-                                rdc.query(),
-                                timeoutMillis,
-                                cancel,
-                                dataPageScanEnabled,
-                                qryInfo
-                            );
-
-                            resIter = new H2FieldsIterator(
-                                res,
-                                mvccTracker,
-                                conn,
-                                r.pageSize(),
-                                log,
-                                h2,
-                                qryInfo,
-                                ctx.tracing()
-                            );
-
-                            conn = null;
-
-                            mvccTracker = null; // To prevent callback inside finally block;
-                        }
-
-                        return new GridQueryCacheObjectsIterator(resIter, h2.objectContext(), keepBinary);
+                        mvccTracker = null; // To prevent callback inside finally block;
                     }
+
+                    return new GridQueryCacheObjectsIterator(resIter, h2.objectContext(), keepBinary);
                 }
                 catch (IgniteCheckedException | RuntimeException e) {
                     release = true;
@@ -589,7 +591,7 @@
                 }
             }
             finally {
-                if (conn != null && (retry || release))
+                if (conn != null && release)
                     U.close(conn, log);
             }
         }
@@ -877,7 +879,7 @@
             }
         }
 
-        final long reqId = qryIdGen.incrementAndGet();
+        final long reqId = qryReqIdGen.incrementAndGet();
 
         h2.runningQueryManager().trackRequestId(reqId);
 
@@ -979,25 +981,28 @@
      */
     void releaseRemoteResources(Collection<ClusterNode> nodes, ReduceQueryRun r, long qryReqId,
         boolean distributedJoins, MvccQueryTracker mvccTracker) {
-        if (distributedJoins)
-            send(nodes, new GridQueryCancelRequest(qryReqId), null, true);
+        try {
+            if (distributedJoins)
+                send(nodes, new GridQueryCancelRequest(qryReqId), null, true);
 
-        for (Reducer idx : r.reducers()) {
-            if (!idx.fetchedAll()) {
-                if (!distributedJoins) // cancel request has been already sent for distributed join.
-                    send(nodes, new GridQueryCancelRequest(qryReqId), null, true);
+            for (Reducer idx : r.reducers()) {
+                if (!idx.fetchedAll()) {
+                    if (!distributedJoins) // cancel request has been already sent for distributed join.
+                        send(nodes, new GridQueryCancelRequest(qryReqId), null, true);
 
-                r.setStateOnException(ctx.localNodeId(),
-                    new CacheException("Query is canceled.", new QueryCancelledException()));
+                    r.setStateOnException(ctx.localNodeId(),
+                        new CacheException("Query is canceled.", new QueryCancelledException()));
 
-                break;
+                    break;
+                }
             }
         }
-
-        if (!runs.remove(qryReqId, r))
-            U.warn(log, "Query run was already removed: " + qryReqId);
-        else if (mvccTracker != null)
-            mvccTracker.onDone();
+        finally {
+            if (!runs.remove(qryReqId, r))
+                U.warn(log, "Query run was already removed: " + qryReqId);
+            else if (mvccTracker != null)
+                mvccTracker.onDone();
+        }
     }
 
     /**
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlRequest.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlRequest.java
index e340b1e0..407dd29 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlRequest.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlRequest.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.internal.GridDirectTransient;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.query.GridCacheQueryMarshallable;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
@@ -346,7 +347,7 @@
 
             if (m instanceof BinaryMarshaller)
                 // To avoid deserializing of enum types.
-                params = ((BinaryMarshaller)m).binaryMarshaller().unmarshal(paramsBytes, ldr);
+                params = BinaryUtils.rawArrayFromBinary(((BinaryMarshaller)m).binaryMarshaller().unmarshal(paramsBytes, ldr));
             else
                 params = U.unmarshal(m, paramsBytes, ldr);
         }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlResponse.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlResponse.java
index 223df69..afd9bf3 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlResponse.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2DmlResponse.java
@@ -23,6 +23,7 @@
 import org.apache.ignite.internal.GridDirectTransient;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.cache.query.GridCacheQueryMarshallable;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
@@ -133,7 +134,7 @@
 
             if (m instanceof BinaryMarshaller)
                 // To avoid deserializing of enum types.
-                errKeys = ((BinaryMarshaller)m).binaryMarshaller().unmarshal(errKeysBytes, ldr);
+                errKeys = BinaryUtils.rawArrayFromBinary(((BinaryMarshaller)m).binaryMarshaller().unmarshal(errKeysBytes, ldr));
             else
                 errKeys = U.unmarshal(m, errKeysBytes, ldr);
         }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2QueryRequest.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2QueryRequest.java
index 561cf18..bed036d 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2QueryRequest.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2QueryRequest.java
@@ -31,11 +31,13 @@
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteCodeGeneratingFail;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
 import org.apache.ignite.internal.processors.cache.query.GridCacheQueryMarshallable;
 import org.apache.ignite.internal.processors.cache.query.GridCacheSqlQuery;
 import org.apache.ignite.internal.processors.cache.query.QueryTable;
+import org.apache.ignite.internal.processors.query.RunningQueryManager;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -159,6 +161,9 @@
     /** TX details holder for {@code SELECT FOR UPDATE}, or {@code null} if not applicable. */
     private GridH2SelectForUpdateTxDetails txReq;
 
+    /** Id of the query assigned by {@link RunningQueryManager} on originator node. */
+    private long qryId;
+
     /** */
     private boolean explicitTimeout;
 
@@ -188,6 +193,7 @@
         schemaName = req.schemaName;
         mvccSnapshot = req.mvccSnapshot;
         txReq = req.txReq;
+        qryId = req.qryId;
         explicitTimeout = req.explicitTimeout;
     }
 
@@ -504,6 +510,27 @@
     }
 
     /**
+     * Id of the query assigned by {@link RunningQueryManager} on originator node.
+     *
+     * @return Query id.
+     */
+    public long queryId() {
+        return qryId;
+    }
+
+    /**
+     * Sets id of the query assigned by {@link RunningQueryManager}.
+     *
+     * @param queryId Query id.
+     * @return {@code this} for chaining.
+     */
+    public GridH2QueryRequest queryId(long queryId) {
+        this.qryId = queryId;
+
+        return this;
+    }
+
+    /**
      * Checks if data page scan enabled.
      *
      * @return {@code true} If data page scan enabled, {@code false} if not, and {@code null} if not set.
@@ -555,7 +582,7 @@
 
             if (m instanceof BinaryMarshaller)
                 // To avoid deserializing of enum types.
-                params = ((BinaryMarshaller)m).binaryMarshaller().unmarshal(paramsBytes, ldr);
+                params = BinaryUtils.rawArrayFromBinary(((BinaryMarshaller)m).binaryMarshaller().unmarshal(paramsBytes, ldr));
             else
                 params = U.unmarshal(m, paramsBytes, ldr);
         }
@@ -666,6 +693,11 @@
 
                 writer.incrementState();
 
+            case 15:
+                if (!writer.writeLong("qryId", qryId))
+                    return false;
+
+                writer.incrementState();
         }
 
         return true;
@@ -799,6 +831,13 @@
 
                 reader.incrementState();
 
+            case 15:
+                qryId = reader.readLong("qryId");
+
+                if (!reader.isLastRead())
+                    return false;
+
+                reader.incrementState();
         }
 
         return reader.afterMessageRead(GridH2QueryRequest.class);
@@ -811,7 +850,7 @@
 
     /** {@inheritDoc} */
     @Override public byte fieldsCount() {
-        return 15;
+        return 16;
     }
 
     /** {@inheritDoc} */
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueEscapedFieldsTest.java
similarity index 73%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueEscapedFieldsTest.java
index a5b6d8f..59d8895 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueEscapedFieldsTest.java
@@ -15,18 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
+package org.apache.ignite.cache.query;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-
-/**
- * Zookeeper IP Finder tests.
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
-
+/** */
+public class IndexQueryCacheKeyValueEscapedFieldsTest extends IndexQueryCacheKeyValueFieldsTest {
+    /** {@inheritDoc} */
+    @Override protected boolean escape() {
+        return true;
+    }
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueFieldsTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueFieldsTest.java
new file mode 100644
index 0000000..3630b89
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryCacheKeyValueFieldsTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.cache.query;
+
+import java.util.LinkedHashMap;
+import java.util.Random;
+import javax.cache.Cache;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gt;
+
+/** The test checks that IndexQuery correctly works with indexes contain single field: cache key or value. */
+public class IndexQueryCacheKeyValueFieldsTest extends GridCommonAbstractTest {
+    /** */
+    private static final String CACHE = "TEST_CACHE";
+
+    /** */
+    private static final int CNT = 10_000;
+
+    /** Whether to escape field names. */
+    protected boolean escape() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        Ignite crd = startGrids(2);
+
+        try (IgniteDataStreamer<String, Integer> streamer = crd.dataStreamer(CACHE)) {
+            for (int i = 0; i < CNT; i++)
+                streamer.addData(key(i), i);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String instanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(instanceName);
+
+        // Cache with signle value field.
+        LinkedHashMap<String, String> fields = new LinkedHashMap<>();
+        fields.put("f1", "java.lang.String");
+        fields.put("f2", "java.lang.Integer");
+
+        QueryEntity qryEntity = new QueryEntity()
+            .setKeyFieldName("f1")
+            .setValueFieldName("f2")
+            .setTableName("TEST")
+            .setKeyType(String.class.getName())
+            .setValueType(Integer.class.getName())
+            .setFields(fields)
+            .setIndexes(F.asList(
+                new QueryIndex("f1"),
+                new QueryIndex("f2"),
+                new QueryIndex("_VAL")));
+
+        return cfg.setCacheConfiguration(
+            new CacheConfiguration<String, Integer>(CACHE)
+                .setSqlEscapeAll(escape())
+                .setQueryEntities(F.asList(qryEntity))
+            );
+    }
+
+    /** */
+    @Test
+    public void testValueAliasField() {
+        check("f2", false);
+    }
+
+    /** */
+    @Test
+    public void testValueAliasUpperField() {
+        if (escape())
+            failOnEscape("F2", false);
+        else
+            check("F2", false);
+    }
+
+    /** */
+    @Test
+    public void testValueField() {
+        check("_VAL", false);
+    }
+
+    /** */
+    @Test
+    public void testValueLowerField() {
+        check("_val", false);
+    }
+
+    /** */
+    @Test
+    public void testKeyAliasField() {
+        check("f1", true);
+    }
+
+    /** */
+    @Test
+    public void testKeyAliasUpperField() {
+        if (escape())
+            failOnEscape("F1", true);
+        else
+            check("F1", true);
+    }
+
+    /** */
+    @Test
+    public void testKeyField() {
+        check("_KEY", true);
+    }
+
+    /** */
+    @Test
+    public void testKeyLowerField() {
+        check("_key", true);
+    }
+
+    /** */
+    private void check(String fld, boolean key) {
+        int pivot = new Random().nextInt(CNT / 2);
+
+        QueryCursor<Cache.Entry<String, Integer>> cursor = grid(0).cache(CACHE).query(
+            new IndexQuery<String, Integer>(Integer.class)
+                .setCriteria(gt(fld, key ? key(pivot) : pivot))
+        );
+
+        assertEquals(CNT - pivot - 1, cursor.getAll().size());
+    }
+
+    /** */
+    private void failOnEscape(String fld, boolean key) {
+        GridTestUtils.assertThrows(null, () -> {
+            grid(0).cache(CACHE).query(
+                new IndexQuery<String, Integer>(Integer.class).setCriteria(gt(fld, key ? key(0) : 0))
+            ).getAll();
+        }, IgniteException.class, "Failed to parse IndexQuery. No index found for criteria.");
+    }
+
+    /** */
+    private static String key(int val) {
+        return String.format("key_%1$5s", val).replace(' ', '0');
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryInlineSizesTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryInlineSizesTest.java
new file mode 100644
index 0000000..16cbef1
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryInlineSizesTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.cache.query;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Random;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.internal.processors.query.h2.database.H2TreeIndexBase;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gt;
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.lt;
+
+/** */
+@RunWith(Parameterized.class)
+public class IndexQueryInlineSizesTest extends GridCommonAbstractTest {
+    /** */
+    private static final String CACHE = "TEST_CACHE";
+
+    /** */
+    private static final String TABLE_CACHE = "TEST_CACHE_TABLE";
+
+    /** */
+    private static final String VALUE_TYPE = "TEST_VALUE_TYPE";
+
+    /** */
+    private static final String TABLE = "TEST_TABLE";
+
+    /** */
+    private static final int CNT = 10_000;
+
+    /** */
+    private static IgniteEx crd;
+
+    /** */
+    @Parameterized.Parameter
+    public int inlineSize;
+
+    /** */
+    @Parameterized.Parameters(name = "inlineSize={0}")
+    public static Iterable<Integer> parameters() {
+        return IntStream.range(0, 20).boxed().collect(Collectors.toList());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        crd = startGrid(2);
+
+        prepareTable(crd);
+    }
+
+    /** */
+    @Test
+    public void testFixedInlineKeys() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld1, fld2")) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld1", low), lt("fld2", high)));
+        }
+    }
+
+    /** */
+    @Test
+    public void testVarInlineKeys() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld1, fld3")) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld1", low), lt("fld3", strFieldVal(high))));
+        }
+    }
+
+    /** */
+    @Test
+    public void testVarInlineKeysFirst() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld3, fld1")) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld1", low), lt("fld3", strFieldVal(high))));
+        }
+    }
+
+    /** */
+    @Test
+    public void testNonInlinedKeys() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld1, fld4")) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld1", low), lt("fld4", new BigDecimal(high))));
+        }
+    }
+
+    /** */
+    @Test
+    public void testNonInlinedKeysFirst() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld4, fld1", 0)) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld1", low), lt("fld4", new BigDecimal(high))));
+        }
+    }
+
+    /** */
+    @Test
+    public void testVarlenAndNonInlined() throws Exception {
+        try (Index idx = new Index(inlineSize, "fld3, fld4")) {
+            check((low, high) -> new IndexQuery<Integer, BinaryObject>(VALUE_TYPE, idx.idxName)
+                .setCriteria(gt("fld3", strFieldVal(low)), lt("fld4", new BigDecimal(high))));
+        }
+    }
+
+    /** */
+    private void check(BiFunction<Integer, Integer, IndexQuery<Integer, BinaryObject>> qryBld) {
+        Random r = new Random();
+
+        int low = r.nextInt(CNT / 2);
+        int high = low + r.nextInt(CNT / 2);
+
+        IndexQuery<Integer, BinaryObject> qry = qryBld.apply(low, high);
+
+        List<?> result = crd.cache(TABLE_CACHE).withKeepBinary().query(qry).getAll();
+
+        assertEquals(high - low - 1, result.size());
+    }
+
+    /** */
+    private void prepareTable(Ignite crd) {
+        SqlFieldsQuery tblQry = new SqlFieldsQuery("create table " + TABLE +
+            "(id int PRIMARY KEY, fld1 int, fld2 int, fld3 varchar, fld4 decimal)" +
+            " with \"CACHE_NAME=" + TABLE_CACHE + ",VALUE_TYPE=" + VALUE_TYPE + "\";");
+
+        crd.getOrCreateCache(CACHE).query(tblQry);
+
+        try (IgniteDataStreamer<Integer, BinaryObject> s = crd.dataStreamer(TABLE_CACHE)) {
+            IntStream.range(0, CNT).forEach((v) -> {
+                BinaryObject bo = crd.binary().builder(VALUE_TYPE)
+                    .setField("fld1", v)
+                    .setField("fld2", v)
+                    .setField("fld3", strFieldVal(v))
+                    .setField("fld4", new BigDecimal(v))
+                    .build();
+
+                s.addData(v, bo);
+            });
+        }
+    }
+
+    /** */
+    private static String strFieldVal(int val) {
+        return String.format("%10s", val).replace(" ", "0");
+    }
+
+    /** */
+    private static class Index implements AutoCloseable {
+        /** */
+        private final String idxName;
+
+        /** */
+        private Index(int inlineSize, String flds) {
+            this(inlineSize, flds, inlineSize);
+        }
+
+        /** */
+        private Index(int inlineSize, String flds, int expInlineSize) {
+            idxName = "IDX_" + inlineSize;
+
+            SqlFieldsQuery idxQry = new SqlFieldsQuery(
+                "create index " + idxName + " on " + TABLE + "(" + flds + ") INLINE_SIZE " + inlineSize);
+
+            crd.cache(CACHE).query(idxQry).getAll();
+
+            GridH2Table tbl = ((IgniteH2Indexing)crd.context().query().getIndexing()).schemaManager()
+                .dataTable("PUBLIC", TABLE);
+
+            assertEquals(expInlineSize, ((H2TreeIndexBase)tbl.getIndex(idxName)).inlineSize());
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() throws Exception {
+            SqlFieldsQuery idxQry = new SqlFieldsQuery("drop index " + idxName);
+
+            crd.cache(CACHE).query(idxQry).getAll();
+        }
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryTestSuite.java
index e141e27..c8982e0 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryTestSuite.java
@@ -28,12 +28,15 @@
     IndexQueryAllTypesTest.class,
     IndexQueryFailoverTest.class,
     IndexQueryFilterTest.class,
+    IndexQueryInlineSizesTest.class,
     IndexQueryKeepBinaryTest.class,
     IndexQueryLocalTest.class,
     IndexQueryQueryEntityTest.class,
     IndexQueryAliasTest.class,
     IndexQuerySqlIndexTest.class,
     IndexQueryRangeTest.class,
+    IndexQueryCacheKeyValueFieldsTest.class,
+    IndexQueryCacheKeyValueEscapedFieldsTest.class,
     IndexQueryWrongIndexTest.class,
     MultifieldIndexQueryTest.class,
     MultiTableIndexQuery.class,
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
index 951529c..de502da 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
@@ -35,6 +35,8 @@
 import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
@@ -499,6 +501,7 @@
     /** */
     private static class Person {
         /** */
+        @GridToStringInclude
         @QuerySqlField(orderedGroups = {
             @QuerySqlField.Group(name = INDEX, order = 0),
             @QuerySqlField.Group(name = DESC_INDEX, order = 0)}
@@ -506,10 +509,12 @@
         final int id;
 
         /** */
+        @GridToStringInclude
         @QuerySqlField(orderedGroups = @QuerySqlField.Group(name = INDEX, order = 1))
         final int secId;
 
         /** */
+        @GridToStringInclude
         @QuerySqlField(orderedGroups = @QuerySqlField.Group(name = DESC_INDEX, order = 1, descending = true))
         final int descId;
 
@@ -544,5 +549,10 @@
         @Override public int hashCode() {
             return Objects.hash(id, secId);
         }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Person.class, this);
+        }
     }
 }
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 7da1a83..b9b58cb 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
@@ -24,6 +24,7 @@
 import org.apache.ignite.internal.client.thin.ComputeTaskTest;
 import org.apache.ignite.internal.client.thin.OptimizedMarshallerClassesCachedTest;
 import org.apache.ignite.internal.client.thin.ReliableChannelTest;
+import org.apache.ignite.internal.client.thin.ServicesBinaryArraysTests;
 import org.apache.ignite.internal.client.thin.ServicesTest;
 import org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessDiscoveryTest;
 import org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessResourceReleaseTest;
@@ -58,6 +59,7 @@
     ClusterApiTest.class,
     ClusterGroupTest.class,
     ServicesTest.class,
+    ServicesBinaryArraysTests.class,
     CacheEntryListenersTest.class,
     ThinClientPartitionAwarenessStableTopologyTest.class,
     ThinClientPartitionAwarenessUnstableTopologyTest.class,
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheAbstractSqlDmlQuerySelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheAbstractSqlDmlQuerySelfTest.java
index 6cbe2b5..72a38b4 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheAbstractSqlDmlQuerySelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheAbstractSqlDmlQuerySelfTest.java
@@ -30,17 +30,17 @@
 import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.binary.AbstractBinaryArraysTest;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.testframework.junits.IgniteTestResources;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 
 /**
  *
  */
 @SuppressWarnings("unchecked")
-public abstract class IgniteCacheAbstractSqlDmlQuerySelfTest extends GridCommonAbstractTest {
+public abstract class IgniteCacheAbstractSqlDmlQuerySelfTest extends AbstractBinaryArraysTest {
     /** */
     protected final Marshaller marsh;
 
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheJoinPartitionedAndReplicatedTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheJoinPartitionedAndReplicatedTest.java
index c607e64..fc1b8750 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheJoinPartitionedAndReplicatedTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheJoinPartitionedAndReplicatedTest.java
@@ -207,6 +207,107 @@
     }
 
     /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSubquery() {
+        Ignite client = grid(2);
+
+        IgniteCache<Object, Object> personCache = client.cache(PERSON_CACHE);
+        IgniteCache<Object, Object> orgCache = client.cache(ORG_CACHE);
+        IgniteCache<Object, Object> orgCacheRepl = client.cache(ORG_CACHE_REPLICATED);
+
+        List<Integer> keys = primaryKeys(ignite(0).cache(PERSON_CACHE), 4, 200_000);
+
+        orgCache.put(keys.get(0), new Organization(0, "org1"));
+        orgCacheRepl.put(keys.get(0), new Organization(0, "org1"));
+
+        personCache.put(keys.get(1), new Person(0, "p1"));
+        personCache.put(keys.get(2), new Person(0, "p2"));
+        personCache.put(keys.get(3), new Person(0, "p3"));
+
+        // Subquery in `WHERE` clause
+        checkQuery("select p._key, p.name " +
+            "from \"person\".Person p where " +
+            "p.orgId in (select o.id from \"org\".Organization o)", orgCache, 3);
+
+        checkQuery("select o.name " +
+            "from \"org\".Organization o where " +
+            "o.id in (select p.orgId from \"person\".Person p)", orgCache, 1);
+
+        checkQuery("select p._key, p.name " +
+            "from \"person\".Person p where " +
+            "p.orgId in (select o.id from \"org\".Organization o)", orgCacheRepl, 3);
+
+        checkQuery("select o.name " +
+            "from \"org\".Organization o where " +
+            "o.id in (select p.orgId from \"person\".Person p)", orgCacheRepl, 1);
+
+        // Prevent `IN` optimization.
+        checkQuery("select p._key, p.name " +
+            "from \"person\".Person p where " +
+            "p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)", orgCache, 3);
+
+        checkQuery("select o.name " +
+            "from \"org\".Organization o where " +
+            "o.id < 10 or o.id in (select p.orgId from \"person\".Person p)", orgCache, 1);
+
+        checkQuery("select p._key, p.name " +
+            "from \"person\".Person p where " +
+            "p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)", orgCacheRepl, 3);
+
+        checkQuery("select o.name " +
+            "from \"org\".Organization o where " +
+            "o.id < 10 or o.id in (select p.orgId from \"person\".Person p)", orgCacheRepl, 1);
+
+        // Subquery in `FROM` clause
+        checkQuery("select p1._key, p1.name " +
+            "from (select p._key, p.name, p.orgId from \"person\".Person p " +
+            "    where p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)) p1 " +
+            "where p1.orgId > -1", orgCache, 3);
+
+        checkQuery("select o1.name " +
+            "from (select o.id, o.name from \"org\".Organization o " +
+            "    where o.id < 10 or o.id in (select p.orgId from \"person\".Person p)) o1 " +
+            "where o1.id > -1", orgCache, 1);
+
+        checkQuery("select p1._key, p1.name " +
+            "from (select p._key, p.name, p.orgId from \"person\".Person p " +
+            "    where p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)) p1 " +
+            "where p1.orgId > -1", orgCacheRepl, 3);
+
+        checkQuery("select o1.name " +
+            "from (select o.id, o.name from \"org\".Organization o " +
+            "    where o.id < 10 or o.id in (select p.orgId from \"person\".Person p)) o1 " +
+            "where o1.id > -1", orgCacheRepl, 1);
+
+        // Join with subquery
+        checkQuery("select o1.name, p._key, p.name " +
+            "from \"person\".Person p " +
+            "join (select o.id, o.name from \"org\".Organization o " +
+            "    where o.id < 10 or o.id in (select p.orgId from \"person\".Person p)) o1 " +
+            "on (p.orgId = o1.id)", orgCache, 3);
+
+        checkQuery("select o.name, p1._key, p1.name " +
+            "from \"org\".Organization o " +
+            "join  (select p._key, p.name, p.orgId from \"person\".Person p " +
+            "    where p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)) p1 " +
+            "on (p1.orgId = o.id)", orgCache, 3);
+
+        checkQuery("select o1.name, p._key, p.name " +
+            "from \"person\".Person p " +
+            "join (select o.id, o.name from \"org\".Organization o " +
+            "    where o.id < 10 or o.id in (select p.orgId from \"person\".Person p)) o1 " +
+            "on (p.orgId = o1.id)", orgCacheRepl, 3);
+
+        checkQuery("select o.name, p1._key, p1.name " +
+            "from \"org\".Organization o " +
+            "join (select p._key, p.name, p.orgId from \"person\".Person p " +
+            "    where p.orgId < 10 or p.orgId in (select o.id from \"org\".Organization o)) p1 " +
+            "on (p1.orgId = o.id)", orgCacheRepl, 3);
+    }
+
+    /**
      * @param sql SQL.
      * @param cache Cache.
      * @param expSize Expected results size.
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheUpdateSqlQuerySelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheUpdateSqlQuerySelfTest.java
index 90de0e6..8e9a23e 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheUpdateSqlQuerySelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/IgniteCacheUpdateSqlQuerySelfTest.java
@@ -462,6 +462,12 @@
         Byte[] bytesCol;
 
         /**
+         * Data bytes array.
+         */
+        @QuerySqlField
+        Person[] personCol;
+
+        /**
          * Data bytes primitive array.
          */
         @QuerySqlField
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ArrayIndexTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ArrayIndexTest.java
index 1225de6..4ba9734 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ArrayIndexTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ArrayIndexTest.java
@@ -21,21 +21,29 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
 import org.apache.ignite.cache.CacheWriteSynchronizationMode;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.ClientConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_USE_BINARY_ARRAYS;
+import static org.apache.ignite.client.Config.SERVER;
+
 /**
  * Checks that sql operation works by arrays.
  */
@@ -161,12 +169,67 @@
 
     }
 
+    /** */
+    @Test
+    public void shouldSupportTableExpressions() throws Exception {
+        checkTableExpression(false);
+    }
+
+    /** */
+    @Test
+    public void shouldSupportTableExpressionsWithBinaryArrays() throws Exception {
+        checkTableExpression(true);
+    }
+
+    /** */
+    private void checkTableExpression(boolean useTypedArrays) throws Exception {
+        System.setProperty(IGNITE_USE_BINARY_ARRAYS, Boolean.toString(useTypedArrays));
+        BinaryArray.initUseBinaryArrays();
+
+        try (IgniteEx ex = startGrid(0);
+             IgniteEx cli = startClientGrid(1);
+             IgniteClient thinCli = Ignition.startClient(new ClientConfiguration().setAddresses(SERVER))) {
+
+            ex.cluster().active(true);
+
+            executeSql(cli, "CREATE TABLE T1 (id int not null, name varchar(1), PRIMARY KEY(id))");
+
+            String insertQry = "INSERT INTO T1(id, name) VALUES (?, ?)";
+
+            executeSql(cli, insertQry, 1, "A");
+            executeSql(cli, insertQry, 2, "B");
+            executeSql(cli, insertQry, 3, "C");
+
+            String select = "SELECT T1.ID, T1.NAME FROM T1 INNER JOIN TABLE (id2 int = ?) T2 on (T1.id = T2.id2) ORDER BY id";
+            Object arg = new Integer[] {1, 2};
+
+            Consumer<List<List<?>>> checker = res -> {
+                assertNotNull(res);
+
+                assertEquals(2, res.size());
+
+                assertEquals(1, res.get(0).get(0));
+                assertEquals("A", res.get(0).get(1));
+                assertEquals(2, res.get(1).get(0));
+                assertEquals("B", res.get(1).get(1));
+            };
+
+            checker.accept(executeSql(ex, select, arg));
+            checker.accept(executeSql(cli, select, arg));
+            checker.accept(thinCli.query(new SqlFieldsQuery(select).setArgs(arg)).getAll());
+        }
+        finally {
+            System.clearProperty(IGNITE_USE_BINARY_ARRAYS);
+            BinaryArray.initUseBinaryArrays();
+        }
+    }
+
     /**
      *
      */
-    private List<List<?>> executeSql(IgniteEx node, String sqlText) throws Exception {
+    private List<List<?>> executeSql(IgniteEx node, String sqlText, Object... args) throws Exception {
         GridQueryProcessor qryProc = node.context().query();
 
-        return qryProc.querySqlFields(new SqlFieldsQuery(sqlText), true).getAll();
+        return qryProc.querySqlFields(new SqlFieldsQuery(sqlText).setArgs(args), true).getAll();
     }
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
index adf264c..9bfac4d 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
@@ -26,15 +26,18 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.UUID;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.cache.CacheException;
 
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.binary.BinaryObjectBuilder;
 import org.apache.ignite.cache.CacheKeyConfiguration;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.QueryIndexType;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
@@ -1524,6 +1527,71 @@
         assertNull(GridTestUtils.getFieldValue(tblDesc1, "luceneIdx"));
     }
 
+    /**
+     * Checks that part of the composite key assembled in BinaryObjectBuilder can pass the validation correctly
+     * if you specify Object type when creating the index.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCacheSecondaryCompositeIndex() throws Exception {
+        inlineSize = 70;
+
+        startGrid();
+
+        String cacheName = "TEST";
+
+        List<QueryIndex> indexes = Collections.singletonList(
+            new QueryIndex("val_obj", QueryIndexType.SORTED).setInlineSize(inlineSize)
+        );
+
+        grid().createCache(new CacheConfiguration<>()
+            .setName(cacheName)
+            .setSqlSchema(cacheName)
+            .setAffinity(new RendezvousAffinityFunction(false, 4))
+            .setQueryEntities(Collections.singleton(new QueryEntity()
+                .setTableName(cacheName)
+                .setKeyType(Integer.class.getName())
+                .setKeyFieldName("id")
+                .setValueType("TEST_VAL_SECONDARY_COMPOSITE")
+                .addQueryField("id", Integer.class.getName(), null)
+                .addQueryField("val_obj", Object.class.getName(), null)
+                .setIndexes(indexes)
+            ))
+        );
+
+        BinaryObjectBuilder bob = grid().binary().builder("TEST_VAL_SECONDARY_COMPOSITE");
+        BinaryObjectBuilder bobInner = grid().binary().builder("inner");
+
+        bobInner.setField("inner_k", 0);
+        bobInner.setField("inner_uuid", UUID.randomUUID());
+
+        bob.setField("val_obj", bobInner.build());
+
+        IgniteCache<Object, Object> cache = grid().cache(cacheName);
+
+        cache.put(0, bob.build());
+
+        BinaryObjectBuilder alice = grid().binary().builder("TEST_VAL_SECONDARY_COMPOSITE");
+        BinaryObjectBuilder aliceInner = grid().binary().builder("inner2");
+
+        aliceInner.setField("inner_k2", 0);
+        aliceInner.setField("inner_uuid2", UUID.randomUUID());
+
+        alice.setField("val_obj", aliceInner.build());
+
+        cache.put(1, alice.build());
+
+        List<List<?>> rows = execSql(cache.withKeepBinary(), "SELECT id, val_obj FROM " + cacheName);
+
+        assertEquals(2, rows.size());
+
+        for (List<?> row : rows) {
+            assertNotNull(row.get(0));
+            assertNotNull("id = " + row.get(0), row.get(1));
+        }
+    }
+
     /** */
     @Test
     public void testCreateSystemIndexWithSpecifiedInlineSizeByApi() throws Exception {
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicSqlTypesIndexTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicSqlTypesIndexTest.java
index 4aedd7f..28e6d57 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicSqlTypesIndexTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicSqlTypesIndexTest.java
@@ -33,6 +33,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.h2.util.DateTimeUtils;
 import org.jetbrains.annotations.Nullable;
@@ -309,25 +310,10 @@
         String idxTypeStr = "BINARY";
         Class<byte[]> idxCls = byte[].class;
 
-        Comparator<byte[]> comp = new Comparator<byte[]>() {
-            @Override public int compare(byte[] o1, byte[] o2) {
-                int res;
-                int len = Math.min(o1.length, o2.length);
-
-                for (int i = 0; i < len; i++) {
-                    if ((res = Byte.compare(o1[i], o2[i])) != 0)
-                        return res;
-                }
-
-                return Integer.compare(o1.length, o2.length);
-            }
-        };
-
-        // TODO: https://issues.apache.org/jira/browse/IGNITE-12313
-//        createPopulateAndVerify(idxTypeStr, idxCls, comp, PK, "BACKUPS=1");
-//        createPopulateAndVerify(idxTypeStr, idxCls, comp, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
-        createPopulateAndVerify(idxTypeStr, idxCls, comp, SECONDARY_DESC, "BACKUPS=1");
-        createPopulateAndVerify(idxTypeStr, idxCls, comp, SECONDARY_ASC, "BACKUPS=1");
+        createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, PK, "BACKUPS=1");
+        createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
+        createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, SECONDARY_DESC, "BACKUPS=1");
+        createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, SECONDARY_ASC, "BACKUPS=1");
     }
 
     /** */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicTableSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicTableSelfTest.java
index 316eba4..4e2d51a 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicTableSelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicTableSelfTest.java
@@ -30,6 +30,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.Callable;
@@ -45,6 +46,7 @@
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.QueryIndex;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
@@ -1806,6 +1808,32 @@
     }
 
     /**
+     * Create table with wrapped key and user value type and insert value by cache API.
+     * Check inserted value.
+     * @throws Exception In case of errors.
+     */
+    @Test
+    public void testWrappedKeyValidation() throws Exception {
+        IgniteCache c1 = ignite(0).getOrCreateCache("WRAP_KEYS");
+        c1.query(new SqlFieldsQuery("CREATE TABLE TestValues (\n" +
+                "  namePK varchar primary key,\n" +
+                "  notUniqueId int\n" +
+                ") WITH \"wrap_key=true," +
+                "value_type=" + TestValue.class.getName() + "\""))
+            .getAll();
+
+        IgniteCache<String, TestValue> values = ignite(0).cache(cacheName("TESTVALUES"));
+
+        TestValue v1 = new TestValue(1);
+
+        values.put("1", v1);
+
+        TestValue rv1 = values.get("1");
+
+        assertEquals(v1, rv1);
+    }
+
+    /**
      * Execute DDL statement on client node.
      *
      * @param sql Statement.
@@ -1952,4 +1980,38 @@
     private static String cacheName(String tblName) {
         return QueryUtils.createTableCacheName("PUBLIC", tblName);
     }
+
+    /**
+     * Test class for sql queryable test value
+     */
+    private static class TestValue {
+        /**
+         * Not unique id
+         */
+        @QuerySqlField
+        int notUniqueId;
+
+        /** */
+        public TestValue(int notUniqueId) {
+            this.notUniqueId = notUniqueId;
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o)
+                return true;
+
+            if (o == null || getClass() != o.getClass())
+                return false;
+
+            TestValue testValue = (TestValue)o;
+
+            return notUniqueId == testValue.notUniqueId;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Objects.hash(notUniqueId);
+        }
+    }
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/PojoIndexLocalQueryTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/PojoIndexLocalQueryTest.java
new file mode 100644
index 0000000..41ae8fc
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/PojoIndexLocalQueryTest.java
@@ -0,0 +1,521 @@
+/*
+ * 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.cache.index;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheKeyConfiguration;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.query.QueryUtils.KEY_FIELD_NAME;
+import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.AFFINITY_KEY_IDX_NAME;
+import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.PK_IDX_NAME;
+
+/**
+ * Basic tests for pojo types of indexed data for tables created through Cache API in LOCAL query mode.
+ */
+public class PojoIndexLocalQueryTest extends AbstractIndexingCommonTest {
+    /** Count of entries that should be preloaded to the cache. */
+    private static final int DATSET_SIZE = 1_000;
+
+    /** Table ID counter. */
+    private static final AtomicInteger TBL_ID = new AtomicInteger();
+
+    /** Template of SELECT query with filtering by range and ordering by indexed column. */
+    private static final String SELECT_ORDERED_RANGE_TEMPLATE =
+        "SELECT val FROM \"%s\" USE INDEX(\"%s\") WHERE %s <= ? ORDER BY idxVal ASC";
+
+    /** Template of SELECT query with filtering by indexed column. */
+    private static final String SELECT_VALUE_TEMPLATE =
+        "SELECT val, idxVal FROM \"%s\" USE INDEX(\"%s\") WHERE %s = ?";
+
+    /** Max length for generator of string values. */
+    private int maxStrLen;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        startGrid(0);
+    }
+
+    /** */
+    @Before
+    public void clearState() {
+        maxStrLen = 40;
+    }
+
+    /** */
+    @Test
+    public void testJavaPojoIndexLocal() {
+        createPopulateAndVerify(TestPojo.class, null, null);
+        createPopulateAndVerify(TestPojo.class, null, TestKeyWithAff.class);
+        createPopulateAndVerify(TestPojo.class, null, TestKeyWithIdx.class);
+    }
+
+    /** */
+    @Test
+    public void localPojoReproducerTest() {
+        String tblName = TestPojo.class.getSimpleName().toUpperCase() + "_TBL" + TBL_ID.incrementAndGet();
+
+        Class<?> keyCls = TestKeyWithIdx.class;
+        Class<?> idxCls = TestPojo.class;
+
+        // Create cache
+        LinkedHashMap<String, String> fields = new LinkedHashMap<>(2);
+
+        fields.put("idxVal", idxCls.getName());
+        fields.put("val", Integer.class.getName());
+
+        QueryEntity qe = new QueryEntity(keyCls.getName(), Integer.class.getName())
+            .setTableName(tblName)
+            .setValueFieldName("val")
+            .setFields(fields);
+
+        String idxName = "IDXVAL_IDX";
+        String idxFieldName = "idxVal";
+
+        qe.setKeyFields(Collections.singleton(idxFieldName));
+        qe.setIndexes(Collections.singleton(new QueryIndex(idxFieldName, true, idxName)));
+
+        IgniteCache<Object, Integer> cache = grid(0).createCache(
+            new CacheConfiguration<Object, Integer>(tblName + "_CACHE")
+                .setKeyConfiguration(new CacheKeyConfiguration((TestKeyWithIdx.class).getName(), "idxVal"))
+                .setQueryEntities(Collections.singletonList(qe)).setSqlSchema("PUBLIC"));
+
+        int[] a1 = {-903, 141, 202, };
+        int[] b1 = {-876, 765, -192, };
+        int[] c1 = {-726, 109, -182, };
+
+        TestPojo pojo1 = new TestPojo(b1[0]);
+        TestPojo pojo2 = new TestPojo(b1[1]);
+        TestPojo pojo3 = new TestPojo(b1[2]);
+
+        TestKeyWithIdx<TestPojo> idx1 = new TestKeyWithIdx<>(a1[0], pojo1);
+        TestKeyWithIdx<TestPojo> idx2 = new TestKeyWithIdx<>(a1[1], pojo2);
+        TestKeyWithIdx<TestPojo> idx3 = new TestKeyWithIdx<>(a1[2], pojo3);
+
+        cache.put(idx1, c1[0]);
+        cache.put(idx2, c1[1]);
+        cache.put(idx3, c1[2]);
+
+        String format = String.format(SELECT_VALUE_TEMPLATE, tblName, idxName, idxFieldName);
+
+        List<List<?>> sqlRes = grid(0).context().query().querySqlFields(
+            new SqlFieldsQuery(format).setArgs(pojo3).setLocal(true), false
+        ).getAll();
+
+        assertEquals(1, sqlRes.size());
+        Integer val = cache.get(idx3);
+        assertEquals(val, sqlRes.get(0).get(0));
+
+        grid(0).destroyCache(tblName + "_CACHE");
+    }
+
+    /**
+     * Executes test scenario: <ul>
+     * <li>Create cache</li>
+     * <li>Populate cache with random data</li>
+     * <li>Verify range query on created table</li>
+     * <li>Verify that table stores the same data as the generated dataset</li>
+     * <li>Destroy cache</li>
+     * </ul>
+     *
+     * @param idxCls Index type class.
+     * @param comp Comparator to sort data set to perform range check.
+     * If {@code null} range check will not be performed.
+     * @param keyCls Key type class. Will be used to generate KEY object
+     * for cache operations. If {@code null} idxCls will be used.
+     * @param <Key> Type of the key in terms of the cache.
+     * @param <Idx> Type of the indexed field.
+     */
+    private <Key extends ClassWrapper, Idx> void createPopulateAndVerify(Class<Idx> idxCls,
+        @Nullable Comparator<Idx> comp, @Nullable Class<Key> keyCls) {
+        Ignite ign = grid(0);
+
+        String tblName = idxCls.getSimpleName().toUpperCase() + "_TBL" + TBL_ID.incrementAndGet();
+
+        try {
+            // Create cache
+            LinkedHashMap<String, String> fields = new LinkedHashMap<>(2);
+
+            fields.put("idxVal", idxCls.getName());
+            fields.put("val", Integer.class.getName());
+
+            QueryEntity qe = new QueryEntity(keyCls == null ? idxCls.getName() : keyCls.getName(), Integer.class.getName())
+                .setTableName(tblName)
+                .setValueFieldName("val")
+                .setFields(fields);
+
+            String idxName;
+            String idxFieldName;
+
+            if (keyCls == null) {
+                qe.setKeyFieldName("idxVal");
+
+                idxName = PK_IDX_NAME;
+                idxFieldName = KEY_FIELD_NAME;
+            }
+            else {
+                idxFieldName = "idxVal";
+
+                qe.setKeyFields(Collections.singleton(idxFieldName));
+
+                if (keyCls.equals(TestKeyWithAff.class))
+                    idxName = AFFINITY_KEY_IDX_NAME;
+                else {
+                    idxName = "IDXVAL_IDX";
+
+                    qe.setIndexes(Collections.singleton(new QueryIndex(idxFieldName, true, idxName)));
+                }
+            }
+
+            IgniteCache<Object, Integer> cache = ign.createCache(
+                new CacheConfiguration<Object, Integer>(tblName + "_CACHE")
+                    .setKeyConfiguration(new CacheKeyConfiguration((keyCls != null ? keyCls : idxCls).getName(), "idxVal"))
+                    .setQueryEntities(Collections.singletonList(qe)).setSqlSchema("PUBLIC"));
+
+            // Then populate it with random data
+            Map<Idx, Integer> data = new TreeMap<>(comp);
+
+            if (keyCls == null)
+                populateTable(data, cache, idxCls);
+            else
+                populateTable(data, cache, keyCls, idxCls);
+
+            // Perform necessary verifications
+            if (comp != null)
+                verifyRange(data, tblName, idxFieldName, idxName, comp);
+
+            verifyEach(data, tblName, idxFieldName, idxName);
+        }
+        finally {
+            // Destroy cache
+            ign.destroyCache(tblName + "_CACHE");
+        }
+    }
+
+    /**
+     * Populate given cache with random data.
+     *
+     * @param data Map which will be used to store all generated data.
+     * @param cache Cache that should be populated.
+     * @param keyCls Class of the key object. Used for generating random value
+     * of the required type.
+     * @param idxCls Class of the indexed value. Used for generating random value
+     * of the required type.
+     * @param <Key> Type of the key object.
+     * @param <Idx> Type of the indexed value.
+     */
+    private <Key, Idx> void populateTable(Map<Idx, Integer> data, IgniteCache<Object, Integer> cache, Class<Key> keyCls,
+        Class<Idx> idxCls) {
+        Map<Idx, Key> idxToKey = new HashMap<>();
+
+        for (int i = 0; i < DATSET_SIZE; i++) {
+            Key key = nextVal(keyCls, idxCls);
+            int val = nextVal(Integer.class, null);
+
+            try {
+                Field f = keyCls.getDeclaredField("idxVal");
+
+                f.setAccessible(true);
+
+                Idx idx = idxCls.cast(f.get(key));
+
+                Key old = idxToKey.put(idx, key);
+
+                if (old != null)
+                    cache.remove(old);
+
+                data.put(idx, val);
+                cache.put(key, val);
+            }
+            catch (Exception ex) {
+                fail("Unable to populate table: " + ex);
+            }
+        }
+    }
+
+    /**
+     * Populate given cache with random data.
+     *
+     * @param data Map which will be used to store all generated data.
+     * @param cache Cache that should be populated.
+     * @param keyCls Class of the key object. Used for generating random value
+     * of the required type.
+     * @param <T> Type of the key object.
+     */
+    private <T> void populateTable(Map<T, Integer> data, IgniteCache<Object, Integer> cache, Class<T> keyCls) {
+        for (int i = 0; i < DATSET_SIZE; i++) {
+            T key = nextVal(keyCls, null);
+            int val = nextVal(Integer.class, null);
+
+            data.put(key, val);
+            cache.put(key, val);
+        }
+    }
+
+    /**
+     * Generates random value for the given class.
+     *
+     * @param cls Class of the required value.
+     * @param innerCls Class of the value for inner object. May be null.
+     * @param <T> Type of the required value.
+     * @param <InnerT> Type of the inner value for complex objects.
+     *
+     * @return Generated value.
+     */
+    private <T, InnerT> T nextVal(Class<T> cls, @Nullable Class<InnerT> innerCls) {
+        if (cls.isAssignableFrom(Boolean.class))
+            return cls.cast(ThreadLocalRandom.current().nextBoolean());
+
+        if (cls.isAssignableFrom(Byte.class))
+            return cls.cast((byte)ThreadLocalRandom.current().nextInt());
+
+        if (cls.isAssignableFrom(Short.class))
+            return cls.cast((short)ThreadLocalRandom.current().nextInt());
+
+        if (cls.isAssignableFrom(Integer.class))
+            return cls.cast(ThreadLocalRandom.current().nextInt());
+
+        if (cls.isAssignableFrom(Long.class))
+            return cls.cast(ThreadLocalRandom.current().nextLong());
+
+        if (cls.isAssignableFrom(Float.class))
+            return cls.cast(ThreadLocalRandom.current().nextFloat());
+
+        if (cls.isAssignableFrom(Double.class))
+            return cls.cast(ThreadLocalRandom.current().nextDouble());
+
+        if (cls.isAssignableFrom(BigDecimal.class))
+            return cls.cast(new BigDecimal(ThreadLocalRandom.current().nextDouble()));
+
+        if (cls.isAssignableFrom(String.class))
+            return cls.cast(GridTestUtils.randomString(ThreadLocalRandom.current(), 1, maxStrLen));
+
+        if (cls.isAssignableFrom(UUID.class))
+            return cls.cast(new UUID(
+                ThreadLocalRandom.current().nextLong(),
+                ThreadLocalRandom.current().nextLong()
+            ));
+
+        if (cls.isAssignableFrom(TestPojo.class))
+            return cls.cast(new TestPojo(ThreadLocalRandom.current().nextInt()));
+
+        if (cls.isAssignableFrom(TestKeyWithIdx.class))
+            return cls.cast(new TestKeyWithIdx<>(
+                ThreadLocalRandom.current().nextInt(),
+                innerCls != null ? nextVal(innerCls, null) : null)
+            );
+
+        if (cls.isAssignableFrom(TestKeyWithAff.class))
+            return cls.cast(new TestKeyWithAff<>(
+                ThreadLocalRandom.current().nextInt(),
+                innerCls != null ? nextVal(innerCls, null) : null)
+            );
+
+        throw new IllegalStateException("There is no generator for class=" + cls.getSimpleName());
+    }
+
+    /**
+     * Verifies range querying.
+     *
+     * @param data Testing dataset.
+     * @param tblName Name of the table from which values should be queried.
+     * @param idxName Name of the index.
+     * @param comp Comparator.
+     * @param <T> Java type mapping of the indexed column.
+     */
+    private <T> void verifyRange(Map<T, Integer> data, String tblName,
+        String idxFieldName, String idxName, Comparator<T> comp) {
+        T val = getRandom(data.keySet());
+
+        List<List<?>> res = execSql(String.format(SELECT_ORDERED_RANGE_TEMPLATE, tblName, idxName, idxFieldName), val);
+
+        List<Integer> exp = data.entrySet().stream()
+            .filter(e -> comp.compare(e.getKey(), val) <= 0)
+            .sorted((e1, e2) -> comp.compare(e1.getKey(), e2.getKey()))
+            .map(Map.Entry::getValue)
+            .collect(Collectors.toList());
+
+        List<Integer> act = res.stream()
+            .flatMap(List::stream)
+            .map(e -> (Integer)e)
+            .collect(Collectors.toList());
+
+        Assert.assertEquals(exp, act);
+    }
+
+    /**
+     * Verifies that table content equals to generated dataset.
+     *
+     * @param data Testing dataset.
+     * @param tblName Name of the table from which values should be queried.
+     * @param idxName Name of the index.
+     * @param <T> Java type mapping of the indexed column.
+     */
+    private <T> void verifyEach(Map<T, Integer> data, String tblName, String idxFieldName, String idxName) {
+        for (Map.Entry<T, Integer> entry : data.entrySet()) {
+            List<List<?>> res = execSql(
+                String.format(SELECT_VALUE_TEMPLATE, tblName, idxName, idxFieldName), entry.getKey()
+            );
+
+            Assert.assertFalse("Result should not be empty", res.isEmpty());
+            Assert.assertFalse("Result should contain at least one column", res.get(0).isEmpty());
+            Assert.assertEquals(entry.getValue(), res.get(0).get(0));
+        }
+    }
+
+    /**
+     * Returns random element from collection.
+     *
+     * @param col Collection from which random element should be returned.
+     * @param <T> Java type mapping of the indexed column.
+     * @return Random element from given collection.
+     */
+    private <T> T getRandom(Collection<T> col) {
+        int rndIdx = ThreadLocalRandom.current().nextInt(col.size());
+
+        int i = 0;
+
+        for (T el : col) {
+            if (i++ == rndIdx)
+                return el;
+        }
+
+        return null;
+    }
+
+    /**
+     * @param qry Query.
+     * @param args Args.
+     */
+    private List<List<?>> execSql(String qry, Object... args) {
+        return grid(0).context().query().querySqlFields(new SqlFieldsQuery(qry).setArgs(args).setLocal(true), false).getAll();
+    }
+
+    /**
+     * Test class for verify index over Java Object.
+     */
+    static class TestPojo implements Comparable<TestPojo> {
+        /** Value. */
+        private final int val;
+
+        /** */
+        public TestPojo(int val) {
+            this.val = val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int compareTo(@NotNull PojoIndexLocalQueryTest.TestPojo o) {
+            if (o == null)
+                return 1;
+
+            return Integer.compare(val, o.val);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o)
+                return true;
+
+            if (o == null || getClass() != o.getClass())
+                return false;
+
+            TestPojo pojo = (TestPojo)o;
+
+            return val == pojo.val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Objects.hash(val);
+        }
+    }
+
+    /**
+     * Interface to limit scope of allowed values
+     * for {@link #createPopulateAndVerify(Class, Comparator, Class)}
+     */
+    interface ClassWrapper {
+    }
+
+    /**
+     * Test class for use like cache key with alternate affinity column.
+     *
+     * @param <T> Type of the affinity column.
+     */
+    static class TestKeyWithAff<T> implements ClassWrapper {
+        /** Value. */
+        private final int val;
+
+        /** Affinity key. */
+        @AffinityKeyMapped
+        private final T idxVal;
+
+        /** */
+        public TestKeyWithAff(int val, T idxVal) {
+            this.val = val;
+            this.idxVal = idxVal;
+        }
+    }
+
+    /**
+     * Test class for use like cache key with additional indexed column.
+     *
+     * @param <T> Type of the additional indexed column.
+     */
+    static class TestKeyWithIdx<T> implements ClassWrapper {
+        /** Value. */
+        private final int val;
+
+        /** Indexed value. */
+        private final T idxVal;
+
+        /** */
+        public TestKeyWithIdx(int val, T idxVal) {
+            this.val = val;
+            this.idxVal = idxVal;
+        }
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/metric/SqlViewExporterSpiTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/metric/SqlViewExporterSpiTest.java
index 09ada17..8d7d3f8 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/metric/SqlViewExporterSpiTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/metric/SqlViewExporterSpiTest.java
@@ -70,6 +70,7 @@
 import org.apache.ignite.internal.processors.service.DummyService;
 import org.apache.ignite.internal.util.StripedExecutor;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.spi.systemview.view.MetastorageView;
@@ -416,6 +417,7 @@
             "SQL_QUERIES",
             "SCAN_QUERIES",
             "NODE_ATTRIBUTES",
+            "SNAPSHOT",
             "TABLES",
             "CLIENT_CONNECTIONS",
             "VIEWS",
@@ -1161,6 +1163,30 @@
             getTestTimeout()));
     }
 
+    /** */
+    @Test
+    public void testSnapshot() throws Exception {
+        String snap0 = "testSnapshot0";
+        String snap1 = "testSnapshot1";
+
+        int nodesCnt = G.allGrids().size();
+
+        assertEquals(0, execute(ignite0, "SELECT * FROM SYS.SNAPSHOT").size());
+
+        ignite0.snapshot().createSnapshot(snap0).get();
+
+        assertEquals(nodesCnt, execute(ignite0, "SELECT * FROM SYS.SNAPSHOT").size());
+
+        ignite0.createCache(DEFAULT_CACHE_NAME).put("key", "val");
+
+        ignite0.snapshot().createSnapshot(snap1).get();
+
+        assertEquals(nodesCnt * 2, execute(ignite0, "SELECT * FROM SYS.SNAPSHOT").size());
+        assertEquals(nodesCnt, execute(ignite0, "SELECT * FROM SYS.SNAPSHOT where name = ?", snap0).size());
+        assertEquals(nodesCnt, execute(ignite0,
+            "SELECT * FROM SYS.SNAPSHOT WHERE cache_groups LIKE '%" + DEFAULT_CACHE_NAME + "%'").size());
+    }
+
     /**
      * Execute query on given node.
      *
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRecoveryTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRecoveryTest.java
index f50698b..f85949a 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRecoveryTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/wal/IgniteWalRecoveryTest.java
@@ -1530,7 +1530,8 @@
                             int realPageSize = data.length;
 
                             pageIO.compactPage(GridUnsafe.wrapPointer(bufPtr, realPageSize), buf, realPageSize);
-                            pageIO.compactPage(ByteBuffer.wrap(data), bufWal, realPageSize);
+                            pageIO.compactPage(ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()),
+                                bufWal, realPageSize);
 
                             bufPtr = GridUnsafe.bufferAddress(buf);
                             data = new byte[bufWal.limit()];
@@ -1726,7 +1727,9 @@
                 else if (rec instanceof DataRecord) {
                     DataRecord dataRecord = (DataRecord)rec;
 
-                    for (DataEntry entry : dataRecord.writeEntries()) {
+                    for (int i = 0; i < dataRecord.entryCount(); i++) {
+                        DataEntry entry = dataRecord.get(i);
+
                         GridCacheVersion txId = entry.nearXidVersion();
 
                         assert activeTransactions.contains(txId) : "No transaction for entry " + entry;
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreMetricsTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreMetricsTest.java
new file mode 100644
index 0000000..55bb94a
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreMetricsTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.cache.persistence.snapshot;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Set;
+import javax.management.AttributeNotFoundException;
+import javax.management.DynamicMBean;
+import javax.management.MBeanException;
+import javax.management.ReflectionException;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.persistence.file.FileIO;
+import org.apache.ignite.internal.processors.cache.persistence.file.FileIOFactory;
+import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager;
+import org.apache.ignite.internal.processors.cache.persistence.file.RandomAccessFileIOFactory;
+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.metric.jmx.JmxMetricExporterSpi;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager.FILE_SUFFIX;
+import static org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager.PART_FILE_PREFIX;
+import static org.apache.ignite.internal.processors.cache.persistence.snapshot.SnapshotRestoreProcess.SNAPSHOT_RESTORE_METRICS;
+
+/**
+ * Tests snapshot restore metrics.
+ */
+public class IgniteClusterSnapshotRestoreMetricsTest extends IgniteClusterSnapshotRestoreBaseTest {
+    /** Separate working directory prefix. */
+    private static final String DEDICATED_DIR_PREFIX = "dedicated-";
+
+    /** Number of nodes using a separate working directory. */
+    private static final int DEDICATED_CNT = 2;
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName)
+            .setMetricExporterSpi(new JmxMetricExporterSpi());
+
+        if (getTestIgniteInstanceIndex(igniteInstanceName) < DEDICATED_CNT) {
+            cfg.setWorkDirectory(Paths.get(U.defaultWorkDirectory(),
+                DEDICATED_DIR_PREFIX + U.maskForFileName(cfg.getIgniteInstanceName())).toString());
+        }
+
+        return cfg;
+    }
+
+    /**
+     * @param name Cache name.
+     * @return Cache configuration.
+     */
+    private CacheConfiguration<Integer, Object> cacheConfig(String name) {
+        return new CacheConfiguration<>(dfltCacheCfg)
+            .setName(name)
+            .setSqlSchema(name)
+            .setQueryEntities(Collections.singletonList(
+                new QueryEntity(Integer.class.getName(), Account.class.getName())
+                .setFields(new LinkedHashMap<>(F.asMap(
+                    "id", Integer.class.getName(),
+                    "balance", Integer.class.getName()))
+                )
+                .setIndexes(Collections.singletonList(new QueryIndex("id"))))
+            );
+    }
+
+    /** {@inheritDoc} */
+    @Before
+    @Override public void beforeTestSnapshot() throws Exception {
+        super.beforeTestSnapshot();
+
+        cleanuo();
+    }
+
+    /** {@inheritDoc} */
+    @After
+    @Override public void afterTestSnapshot() throws Exception {
+        super.afterTestSnapshot();
+
+        cleanuo();
+    }
+
+    /** @throws Exception If fails. */
+    @Test
+    public void testRestoreSnapshotProgress() throws Exception {
+        // Caches with differebt partition distribution.
+        CacheConfiguration<Integer, Object> ccfg1 = cacheConfig("cache1").setBackups(0);
+        CacheConfiguration<Integer, Object> ccfg2 = cacheConfig("cache2").setCacheMode(CacheMode.REPLICATED);
+
+        Ignite ignite = startGridsWithCache(DEDICATED_CNT, CACHE_KEYS_RANGE, key -> new Account(key, key), ccfg1, ccfg2);
+
+        ignite.snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
+
+        ignite.destroyCaches(F.asList(ccfg1.getName(), ccfg2.getName()));
+        awaitPartitionMapExchange();
+
+        // Add new empty node.
+        IgniteEx emptyNode = startGrid(DEDICATED_CNT);
+        resetBaselineTopology();
+
+        checkMetricsDefaults();
+
+        Set<String> grpNames = new HashSet<>(F.asList(ccfg1.getName(), ccfg2.getName()));
+
+        ignite.snapshot().restoreSnapshot(SNAPSHOT_NAME, grpNames);
+
+        for (Ignite grid : G.allGrids()) {
+            DynamicMBean mReg = metricRegistry(grid.name(), null, SNAPSHOT_RESTORE_METRICS);
+            String nodeNameMsg = "node=" + grid.name();
+
+            assertTrue(nodeNameMsg, GridTestUtils.waitForCondition(() -> getNumMetric("endTime", mReg) > 0, TIMEOUT));
+
+            int expParts = ((IgniteEx)grid).cachex(ccfg1.getName()).context().topology().localPartitions().size() +
+                ((IgniteEx)grid).cachex(ccfg2.getName()).context().topology().localPartitions().size();
+
+            // Cache2 is replicated - the index partition is being copied (on snapshot data nodes).
+            if (!emptyNode.name().equals(grid.name()))
+                expParts += 1;
+
+            assertEquals(nodeNameMsg, SNAPSHOT_NAME, mReg.getAttribute("snapshotName"));
+            assertEquals(nodeNameMsg, "", mReg.getAttribute("error"));
+
+            assertFalse(nodeNameMsg, ((String)mReg.getAttribute("requestId")).isEmpty());
+
+            assertEquals(nodeNameMsg, expParts, getNumMetric("totalPartitions", mReg));
+            assertEquals(nodeNameMsg, expParts, getNumMetric("processedPartitions", mReg));
+
+            long startTime = getNumMetric("startTime", mReg);
+            long endTime = getNumMetric("endTime", mReg);
+
+            assertTrue(nodeNameMsg, startTime > 0);
+            assertTrue(nodeNameMsg, endTime >= startTime);
+        }
+
+        assertSnapshotCacheKeys(ignite.cache(ccfg1.getName()));
+        assertSnapshotCacheKeys(ignite.cache(ccfg2.getName()));
+    }
+
+    /** @throws Exception If fails. */
+    @Test
+    public void testRestoreSnapshotError() throws Exception {
+        dfltCacheCfg.setCacheMode(CacheMode.REPLICATED);
+
+        IgniteEx ignite = startGridsWithSnapshot(2, CACHE_KEYS_RANGE);
+
+        String failingFilePath = Paths.get(FilePageStoreManager.cacheDirName(dfltCacheCfg),
+            PART_FILE_PREFIX + (dfltCacheCfg.getAffinity().partitions() / 2) + FILE_SUFFIX).toString();
+
+        FileIOFactory ioFactory = new RandomAccessFileIOFactory();
+        String testErrMsg = "Test exception";
+
+        ignite.context().cache().context().snapshotMgr().ioFactory((file, modes) -> {
+            FileIO delegate = ioFactory.create(file, modes);
+
+            if (file.getPath().endsWith(failingFilePath))
+                throw new RuntimeException(testErrMsg);
+
+            return delegate;
+        });
+
+        checkMetricsDefaults();
+
+        ignite.snapshot().restoreSnapshot(SNAPSHOT_NAME, null);
+
+        for (Ignite grid : G.allGrids()) {
+            DynamicMBean mReg = metricRegistry(grid.name(), null, SNAPSHOT_RESTORE_METRICS);
+
+            String nodeNameMsg = "node=" + grid.name();
+
+            assertTrue(nodeNameMsg, GridTestUtils.waitForCondition(() -> getNumMetric("endTime", mReg) > 0, TIMEOUT));
+
+            long startTime = getNumMetric("startTime", mReg);
+            long endTime = getNumMetric("endTime", mReg);
+
+            assertEquals(nodeNameMsg, SNAPSHOT_NAME, mReg.getAttribute("snapshotName"));
+
+            assertFalse(nodeNameMsg, ((String)mReg.getAttribute("requestId")).isEmpty());
+
+            assertTrue(nodeNameMsg, startTime > 0);
+            assertTrue(nodeNameMsg, endTime >= startTime);
+            assertTrue(nodeNameMsg, ((String)mReg.getAttribute("error")).contains(testErrMsg));
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    private void checkMetricsDefaults() throws Exception {
+        for (Ignite grid : G.allGrids()) {
+            String nodeNameMsg = "node=" + grid.name();
+
+            DynamicMBean mReg = metricRegistry(grid.name(), null, SNAPSHOT_RESTORE_METRICS);
+
+            assertEquals(nodeNameMsg, 0, getNumMetric("endTime", mReg));
+            assertEquals(nodeNameMsg, -1, getNumMetric("totalPartitions", mReg));
+            assertEquals(nodeNameMsg, 0, getNumMetric("processedPartitions", mReg));
+            assertTrue(nodeNameMsg, String.valueOf(mReg.getAttribute("snapshotName")).isEmpty());
+        }
+    }
+
+    /**
+     * @param mBean Ignite snapshot restore MBean.
+     * @param name Metric name.
+     * @return Metric value.
+     */
+    private long getNumMetric(String name, DynamicMBean mBean) {
+        try {
+            return ((Number)mBean.getAttribute(name)).longValue();
+        }
+        catch (MBeanException | ReflectionException | AttributeNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    private void cleanuo() throws Exception {
+        FilenameFilter filter = (file, name) -> file.isDirectory() && name.startsWith(DEDICATED_DIR_PREFIX);
+
+        for (File file : new File(U.defaultWorkDirectory()).listFiles(filter))
+            U.delete(file);
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreWithIndexingTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreWithIndexingTest.java
index 7db3ff6..e1bc45d 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreWithIndexingTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteClusterSnapshotRestoreWithIndexingTest.java
@@ -159,7 +159,7 @@
         SnapshotEvent startEvt = evts.get(0);
 
         assertEquals(SNAPSHOT_NAME, startEvt.snapshotName());
-        assertTrue(startEvt.message().contains("grps=[" + DEFAULT_CACHE_NAME + ']'));
+        assertTrue(startEvt.message().contains("caches=[" + DEFAULT_CACHE_NAME + ']'));
     }
 
     /** {@inheritDoc} */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java
index eb9bc23..6e5cbba 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java
@@ -1797,7 +1797,6 @@
     /**
      *
      */
-    @Ignore("https://issues.apache.org/jira/browse/IGNITE-1886")
     @Test
     public void testFunctionNpe() {
         IgniteCache<Integer, User> userCache = ignite(0).createCache(
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/SqlSystemViewsSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/SqlSystemViewsSelfTest.java
index f88b2dc8..f4b4e4a 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/SqlSystemViewsSelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/SqlSystemViewsSelfTest.java
@@ -53,6 +53,7 @@
 import org.apache.ignite.cache.query.SqlQuery;
 import org.apache.ignite.cluster.ClusterMetrics;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
@@ -85,6 +86,7 @@
 
 import static java.util.Arrays.asList;
 import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage.METASTORAGE_CACHE_NAME;
 import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
 import static org.junit.Assert.assertNotEquals;
 
@@ -1065,6 +1067,66 @@
         assertEquals("val0", res.get(0).get(2));
     }
 
+    /**
+     * Test snapshots system view.
+     */
+    @Test
+    public void testSnapshotViews() throws Exception {
+        String node0 = "node0";
+        String node1 = "node1";
+
+        String testSnapname = "testSnapshot";
+        String testSnapname0 = "testSnapshot0";
+        String testCache = "testCache";
+
+        Ignite ignite = startGrid(getTestIgniteInstanceName(), getPdsConfiguration(node0));
+        startGrid(getTestIgniteInstanceName(1), getPdsConfiguration(node1));
+
+        int nodesCnt = G.allGrids().size();
+
+        ignite.cluster().state(ClusterState.ACTIVE);
+        ignite.snapshot().createSnapshot(testSnapname).get();
+
+        List<List<?>> res = execSql("SELECT * FROM " + systemSchemaName() + ".SNAPSHOT");
+
+        assertColumnTypes(res.get(0), String.class, String.class, String.class, String.class);
+
+        assertEquals(nodesCnt, res.size());
+
+        assertTrue(res.stream().map(l -> l.get(0)).allMatch(testSnapname::equals));
+
+        res = execSql("SELECT BASELINE_NODES FROM " + systemSchemaName() + ".SNAPSHOT WHERE CONSISTENT_ID = ?", node0);
+
+        assertEquals(1, res.size());
+
+        ignite.createCache(testCache);
+
+        ignite.snapshot().createSnapshot(testSnapname0).get();
+
+        res = execSql("SELECT * FROM " + systemSchemaName() + ".SNAPSHOT");
+
+        assertEquals(nodesCnt * 2, res.size());
+
+        String expBltNodes = F.concat(asList(node0, node1), ",");
+
+        assertTrue(res.stream().map(l -> l.get(2)).allMatch(expBltNodes::equals));
+
+        res = execSql("SELECT NAME FROM " + systemSchemaName() + ".SNAPSHOT WHERE CONSISTENT_ID = ?", node0);
+
+        assertEquals(2, res.size());
+
+        res = execSql("SELECT NAME, CACHE_GROUPS FROM " + systemSchemaName() + ".SNAPSHOT " +
+            "WHERE NAME = ?", testSnapname0);
+
+        assertEquals(nodesCnt, res.size());
+
+        assertEquals(testSnapname0, res.get(0).get(0));
+
+        String expCacheGrps = F.concat(asList(DEFAULT_CACHE_NAME, testCache, METASTORAGE_CACHE_NAME), ",");
+
+        assertEquals(expCacheGrps, res.get(0).get(1));
+    }
+
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration() throws Exception {
         IgniteConfiguration cfg = super.getConfiguration()
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizerSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizerSelfTest.java
index dc4a381..e137cac 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizerSelfTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/GridSubqueryJoinOptimizerSelfTest.java
@@ -18,10 +18,12 @@
 package org.apache.ignite.internal.processors.query.h2;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Random;
+
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
@@ -894,6 +896,51 @@
     }
 
     /**
+     * Case when select subquery is under EXISTS operator.
+     */
+    @Test
+    public void testExistsOperatorWithSubqueryUnderSelect() {
+        String outerSqlTemplate = "SELECT (EXISTS (%s));";
+        String subSql1 = "SELECT id FROM dep WHERE id = 1";
+        String subSql2 = "SELECT 1 FROM dep WHERE id = 1";
+
+        check(String.format(outerSqlTemplate, subSql1), 2);
+
+        check(String.format(outerSqlTemplate, subSql2), 2);
+    }
+
+    /**
+     * Case when select subquery is under different possible operators.
+     */
+    @Test
+    public void testDifferentOperatorsWithSubqueryUnderSelect() {
+        String outerSqlTemplate = "SELECT (%s);";
+
+        List<String> subSelects = new ArrayList<>();
+
+        subSelects.add("(SELECT %s FROM dep WHERE id = 1) IN (1,-1)");
+        subSelects.add("(SELECT %s FROM dep WHERE id = 1) IS NOT NULL");
+        subSelects.add("(SELECT %s FROM dep WHERE id = 1) IS 1");
+        subSelects.add("(SELECT %s FROM dep WHERE id = 1) < 0");
+        subSelects.add("-(SELECT %s FROM dep WHERE id = 1)");
+        subSelects.add("(SELECT %s FROM dep WHERE id = 1) = 2");
+
+        for (String param : Arrays.asList("1", "2")) {
+            for (String subSelect : subSelects) {
+                String formattedSubSelect = String.format(subSelect, param);
+
+                check(String.format(outerSqlTemplate, formattedSubSelect), 2);
+            }
+        }
+
+        for (String subSelect : subSelects) {
+            String formattedSubSelect = String.format(subSelect, "id");
+
+             check(String.format(outerSqlTemplate, formattedSubSelect), 1);
+        }
+    }
+
+    /**
      * @param sql Sql.
      * @param expSelectClauses Expected select clauses.
      */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/systemview/JmxExporterSpiTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/systemview/JmxExporterSpiTest.java
new file mode 100644
index 0000000..922eccc
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/systemview/JmxExporterSpiTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.systemview;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.TabularDataSupport;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.managers.systemview.JmxSystemViewExporterSpi;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.managers.systemview.SystemViewMBean.VIEWS;
+import static org.apache.ignite.internal.processors.query.h2.SchemaManager.SQL_TBL_COLS_VIEW;
+
+/**
+ * Tests {@link JmxSystemViewExporterSpi}.
+ */
+public class JmxExporterSpiTest extends GridCommonAbstractTest {
+    /** @throws Exception If failed. */
+    @Test
+    public void testTableColumns() throws Exception {
+        String tableName = "TEST";
+
+        IgniteEx ignite = startGrid(getConfiguration().setCacheConfiguration(
+            new CacheConfiguration<>(DEFAULT_CACHE_NAME)
+                .setQueryEntities(F.asList(
+                    new QueryEntity()
+                        .setTableName(tableName)
+                        .setKeyFieldName("ID")
+                        .setValueType(Integer.class.getName())
+                        .addQueryField("ID", Integer.class.getName(), null)))));
+
+        Map<String, String> expTypes = new HashMap<>();
+
+        expTypes.put("_KEY", null);
+        expTypes.put("_VAL", null);
+        expTypes.put("ID", Integer.class.getName());
+
+        TabularDataSupport columns =
+            (TabularDataSupport)metricRegistry(ignite.name(), VIEWS, SQL_TBL_COLS_VIEW).getAttribute(VIEWS);
+
+        columns.values().stream().map(data -> (CompositeData)data)
+            .filter(data -> tableName.equals(data.get("tableName")))
+            .forEach(data -> {
+                String columnName = (String)data.get("columnName");
+
+                assertTrue("Unexpected column: " + columnName, expTypes.containsKey(columnName));
+                assertEquals(expTypes.remove(columnName), data.get("type"));
+            });
+
+        assertTrue("Expected columns: " + expTypes.keySet(), expTypes.isEmpty());
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlByteArrayTest.java b/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlByteArrayTest.java
new file mode 100644
index 0000000..e48ff87
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlByteArrayTest.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.sqltests;
+
+import java.util.Arrays;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.binary.AbstractBinaryArraysTest;
+import org.apache.ignite.internal.util.typedef.F;
+import org.junit.Test;
+
+/** Test to check CRUD with array keys and values. */
+public class SqlByteArrayTest extends AbstractBinaryArraysTest {
+    /** */
+    private static Ignite server;
+
+    /** */
+    private static Ignite client;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        server = startGrid(0);
+        client = startClientGrid(1);
+    }
+
+    /** */
+    @Test
+    public void testSQLByteArrayValue() {
+        doTest(server, byte[].class, new byte[] {1, 2}, new byte[] {1, 2, 3});
+        doTest(client, byte[].class, new byte[] {1, 2}, new byte[] {1, 2, 3});
+    }
+
+    /** */
+    private <T> void doTest(Ignite ignite, Class<T> cls, T val1, T val2) {
+        ignite.destroyCache("array-cache");
+
+        CacheConfiguration<Integer, Object> ccfg = new CacheConfiguration<Integer, Object>()
+            .setName("array-cache")
+            .setQueryEntities(Arrays.asList(
+                new QueryEntity(Integer.class.getName(), cls.getName())
+                    .setTableName("array_table")));
+
+        IgniteCache<Integer, Object> cache = ignite.createCache(ccfg);
+
+        cache.query(new SqlFieldsQuery("INSERT INTO array_table (_key, _val) VALUES (?, ?)").setArgs(2, val1)).getAll();
+
+        assertTrue(cache.containsKey(2));
+        assertEquals(0, F.compareArrays(val1, cache.get(2)));
+
+        cache.query(new SqlFieldsQuery("UPDATE array_table SET _val = ? WHERE _key = ? ").setArgs(val2, 2)).getAll();
+
+        assertTrue(cache.containsKey(2));
+        assertEquals(0, F.compareArrays(val2, cache.get(2)));
+
+        cache.query(new SqlFieldsQuery("DELETE FROM array_table WHERE _key = ?").setArgs(2)).getAll();
+
+        assertFalse(cache.containsKey(2));
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlDataTypesCoverageTests.java b/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlDataTypesCoverageTests.java
index 7ffbdbf..2e92b9f 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlDataTypesCoverageTests.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/sqltests/SqlDataTypesCoverageTests.java
@@ -255,7 +255,6 @@
      *
      * @throws Exception If failed.
      */
-    @Ignore("https://issues.apache.org/jira/browse/IGNITE-12313")
     @SuppressWarnings("ZeroLengthArrayAllocation")
     @Test
     public void testBinaryDataType() throws Exception {
@@ -363,6 +362,15 @@
             ignite.context().query().querySqlFields(
                 new SqlFieldsQuery("UPDATE " + tblName + " SET val =  " + revertedValToPut + ";"), false);
 
+            if (writeSyncMode == CacheWriteSynchronizationMode.FULL_ASYNC &&
+                !waitForCondition(() -> ignite.context().query().querySqlFields(
+                    new SqlFieldsQuery("SELECT val FROM " + tblName), false).getAll().stream()
+                        .allMatch(r -> r.get(0) instanceof byte[]
+                            ? Arrays.equals((byte[])expRevertedVal, (byte[])r.get(0))
+                            : r.get(0).equals(expRevertedVal)),
+                    TIMEOUT_FOR_KEY_RETRIEVAL_IN_FULL_ASYNC_MODE))
+                fail("Unable to retrieve data via SELECT.");
+
             // Check UPDATE/SELECT
             check(ignite, "SELECT id, val FROM " + tblName + ";", dataType, expVal, expRevertedVal);
 
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite.java
index c1a6f79..45aee87 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite.java
@@ -177,6 +177,7 @@
 import org.apache.ignite.internal.processors.cache.index.IndexMetricsTest;
 import org.apache.ignite.internal.processors.cache.index.LongIndexNameTest;
 import org.apache.ignite.internal.processors.cache.index.OptimizedMarshallerIndexNameTest;
+import org.apache.ignite.internal.processors.cache.index.PojoIndexLocalQueryTest;
 import org.apache.ignite.internal.processors.cache.index.QueryEntityValidationSelfTest;
 import org.apache.ignite.internal.processors.cache.index.SchemaExchangeSelfTest;
 import org.apache.ignite.internal.processors.cache.index.SqlPartitionEvictionTest;
@@ -655,6 +656,7 @@
 
     BasicSqlTypesIndexTest.class,
     BasicJavaTypesIndexTest.class,
+    PojoIndexLocalQueryTest.class,
 
     //Cancellation of queries.
     KillQueryTest.class,
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite2.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite2.java
index 5e1e560..e41c949 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite2.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteBinaryCacheQueryTestSuite2.java
@@ -85,6 +85,7 @@
 import org.apache.ignite.internal.processors.query.h2.twostep.RetryCauseMessageSelfTest;
 import org.apache.ignite.internal.processors.query.h2.twostep.TableViewSubquerySelfTest;
 import org.apache.ignite.internal.processors.query.timeout.DefaultQueryTimeoutTestSuite;
+import org.apache.ignite.sqltests.SqlByteArrayTest;
 import org.apache.ignite.sqltests.SqlDataTypesCoverageTests;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -195,6 +196,7 @@
     SqlPartOfComplexPkLookupTest.class,
 
     SqlDataTypesCoverageTests.class,
+    SqlByteArrayTest.class,
     SqlPartOfComplexPkLookupTest.class,
 
     IgniteCacheLocalQueryDefaultTimeoutSelfTest.class,
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgnitePdsWithIndexingTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgnitePdsWithIndexingTestSuite.java
index acc21f4..965a881 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgnitePdsWithIndexingTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgnitePdsWithIndexingTestSuite.java
@@ -32,7 +32,6 @@
 import org.apache.ignite.internal.processors.cache.persistence.db.IndexingMultithreadedLoadContinuousRestartTest;
 import org.apache.ignite.internal.processors.cache.persistence.db.LongDestroyDurableBackgroundTaskTest;
 import org.apache.ignite.internal.processors.cache.persistence.db.MultipleParallelCacheDeleteDeadlockTest;
-import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteClusterSnapshotRestoreWithIndexingTest;
 import org.apache.ignite.internal.processors.database.IgniteDbMultiNodeWithIndexingPutGetTest;
 import org.apache.ignite.internal.processors.database.IgniteDbSingleNodeWithIndexingPutGetTest;
 import org.apache.ignite.internal.processors.database.IgniteDbSingleNodeWithIndexingWalRestoreTest;
@@ -68,7 +67,6 @@
     IgnitePdsIndexingDefragmentationTest.class,
     StopRebuildIndexTest.class,
     ForceRebuildIndexTest.class,
-    IgniteClusterSnapshotRestoreWithIndexingTest.class,
     ResumeRebuildIndexTest.class,
     ResumeCreateIndexTest.class,
     RenameIndexTreeTest.class,
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteSnapshotWithIndexingTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteSnapshotWithIndexingTestSuite.java
index 4696a78..40d6654 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteSnapshotWithIndexingTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteSnapshotWithIndexingTestSuite.java
@@ -18,6 +18,8 @@
 package org.apache.ignite.testsuites;
 
 import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteClusterSnapshotCheckWithIndexesTest;
+import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteClusterSnapshotRestoreMetricsTest;
+import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteClusterSnapshotRestoreWithIndexingTest;
 import org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteClusterSnapshotWithIndexesTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -26,7 +28,9 @@
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
     IgniteClusterSnapshotWithIndexesTest.class,
-    IgniteClusterSnapshotCheckWithIndexesTest.class
+    IgniteClusterSnapshotCheckWithIndexesTest.class,
+    IgniteClusterSnapshotRestoreWithIndexingTest.class,
+    IgniteClusterSnapshotRestoreMetricsTest.class
 })
 public class IgniteSnapshotWithIndexingTestSuite {
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteStatisticsTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteStatisticsTestSuite.java
index ef338cd..d65e55f 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteStatisticsTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteStatisticsTestSuite.java
@@ -46,6 +46,7 @@
 import org.apache.ignite.internal.sql.SqlParserAnalyzeSelfTest;
 import org.apache.ignite.internal.sql.SqlParserDropStatisticsSelfTest;
 import org.apache.ignite.internal.sql.SqlParserRefreshStatisticsSelfTest;
+import org.apache.ignite.internal.systemview.JmxExporterSpiTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -93,7 +94,8 @@
 
     // Views
     StatisticsViewsPersistenceTest.class,
-    StatisticsViewsInMemoryTest.class
+    StatisticsViewsInMemoryTest.class,
+    JmxExporterSpiTest.class
 })
 public class IgniteStatisticsTestSuite {
 }
diff --git a/modules/log4j2/src/main/java/org/apache/ignite/logger/log4j2/Log4J2Logger.java b/modules/log4j2/src/main/java/org/apache/ignite/logger/log4j2/Log4J2Logger.java
index 66050c2..954c7c8 100644
--- a/modules/log4j2/src/main/java/org/apache/ignite/logger/log4j2/Log4J2Logger.java
+++ b/modules/log4j2/src/main/java/org/apache/ignite/logger/log4j2/Log4J2Logger.java
@@ -18,7 +18,6 @@
 package org.apache.ignite.logger.log4j2;
 
 import java.io.File;
-import java.lang.reflect.Field;
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.util.Map;
@@ -237,27 +236,18 @@
                     return ((RollingFileAppender)a).getFileName();
 
                 if (a instanceof RoutingAppender) {
-                    try {
-                        RoutingAppender routing = (RoutingAppender)a;
+                    RoutingAppender routing = (RoutingAppender)a;
 
-                        Field appsFiled = routing.getClass().getDeclaredField("appenders");
+                    Map<String, AppenderControl> appenders = routing.getAppenders();
 
-                        appsFiled.setAccessible(true);
+                    for (AppenderControl control : appenders.values()) {
+                        Appender innerApp = control.getAppender();
 
-                        Map<String, AppenderControl> appenders = (Map<String, AppenderControl>)appsFiled.get(routing);
+                        if (innerApp instanceof FileAppender)
+                            return normalize(((FileAppender)innerApp).getFileName());
 
-                        for (AppenderControl control : appenders.values()) {
-                            Appender innerApp = control.getAppender();
-
-                            if (innerApp instanceof FileAppender)
-                                return normalize(((FileAppender)innerApp).getFileName());
-
-                            if (innerApp instanceof RollingFileAppender)
-                                return normalize(((RollingFileAppender)innerApp).getFileName());
-                        }
-                    }
-                    catch (IllegalAccessException | NoSuchFieldException e) {
-                        error("Failed to get file name (was the implementation of log4j2 changed?).", e);
+                        if (innerApp instanceof RollingFileAppender)
+                            return normalize(((RollingFileAppender)innerApp).getFileName());
                     }
                 }
             }
diff --git a/modules/mesos/pom.xml b/modules/mesos/pom.xml
index 2bdd0d5..ae11ef3 100644
--- a/modules/mesos/pom.xml
+++ b/modules/mesos/pom.xml
@@ -35,7 +35,7 @@
     <url>http://ignite.apache.org</url>
 
     <properties>
-        <mesos.version>1.5.0</mesos.version>
+        <mesos.version>1.11.0</mesos.version>
     </properties>
 
     <dependencies>
diff --git a/modules/mesos/src/test/java/org/apache/ignite/mesos/IgniteSchedulerSelfTest.java b/modules/mesos/src/test/java/org/apache/ignite/mesos/IgniteSchedulerSelfTest.java
index 8fc4da2..652c267 100644
--- a/modules/mesos/src/test/java/org/apache/ignite/mesos/IgniteSchedulerSelfTest.java
+++ b/modules/mesos/src/test/java/org/apache/ignite/mesos/IgniteSchedulerSelfTest.java
@@ -25,6 +25,7 @@
 import org.apache.ignite.mesos.resource.ResourceProvider;
 import org.apache.mesos.Protos;
 import org.apache.mesos.SchedulerDriver;
+import org.apache.mesos.scheduler.Protos.OfferConstraints;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -506,11 +507,17 @@
         }
 
         /** {@inheritDoc} */
+        @Override public Protos.Status reviveOffers(Collection<String> collection) { return null; }
+
+        /** {@inheritDoc} */
         @Override public Protos.Status suppressOffers() {
             return null;
         }
 
         /** {@inheritDoc} */
+        @Override public Protos.Status suppressOffers(Collection<String> collection) { return null; }
+
+        /** {@inheritDoc} */
         @Override public Protos.Status acknowledgeStatusUpdate(Protos.TaskStatus status) {
             return null;
         }
@@ -525,5 +532,14 @@
         @Override public Protos.Status reconcileTasks(Collection<Protos.TaskStatus> statuses) {
             return null;
         }
+
+        /** {@inheritDoc} */
+        @Override public Protos.Status updateFramework(Protos.FrameworkInfo frameworkInfo, Collection<String> collection,
+                                                       OfferConstraints offerConstraints) {
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Protos.Status updateFramework(Protos.FrameworkInfo frameworkInfo, Collection<String> collection) { return null; }
     }
 }
diff --git a/modules/ml/pom.xml b/modules/ml/pom.xml
index 50b3162..e323944 100644
--- a/modules/ml/pom.xml
+++ b/modules/ml/pom.xml
@@ -167,7 +167,7 @@
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
-            <version>1.7.7</version>
+            <version>${slf4j.version}</version>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
diff --git a/modules/ml/spark-model-parser/pom.xml b/modules/ml/spark-model-parser/pom.xml
index aa03b11..9595d84 100644
--- a/modules/ml/spark-model-parser/pom.xml
+++ b/modules/ml/spark-model-parser/pom.xml
@@ -86,6 +86,12 @@
             <groupId>org.apache.hadoop</groupId>
             <artifactId>hadoop-common</artifactId>
             <version>2.9.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
diff --git a/modules/numa-allocator/README.md b/modules/numa-allocator/README.md
new file mode 100644
index 0000000..89abbc4
--- /dev/null
+++ b/modules/numa-allocator/README.md
@@ -0,0 +1,141 @@
+# NUMA aware memory allocator for Apache Ignite
+Allocates memory on linux using `libnuma` under the hood. 
+
+## Requirements
+`libnuma 2.0.x` must be installed on your Linux machine. It is recommended to install `numactl` utility also.
+1. `RHEL` or `Cent OS`
+```bash
+$ sudo yum install numactl
+```
+2. `Ubuntu` or `Debian`
+```bash
+$ sudo apt install numactl
+```
+
+## Building from source
+`JDK`, `C++ 11` compatible compiler (`gcc >= 4.8.5`) and `libnuma` headers
+1. `RHEL` or `Cent OS`
+```bash
+$ sudo yum groupinstall 'Development Tools'
+$ sudo yum install java-1.8.0-openjdk numactl-devel libstdc++-static
+```
+2. `Ubuntu` or `Debian`
+```bash
+$ sudo apt install build-essential libnuma-dev openjdk-8-jdk
+```
+## Usage
+### Simple allocation strategy
+1. Allocation with default NUMA policy for thread/process, uses `void *numa_alloc(size_t)` under the hood:
+```xml
+<property name="dataStorageConfiguration">
+    <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
+        <property name="defaultDataRegionConfiguration">
+            <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
+                <property name="name" value="Default_Region"/>
+                ....
+                <property name="memoryAllocator">
+                    <bean class="org.apache.ignite.mem.NumaAllocator">
+                        <constructor-arg>
+                            <bean class="org.apache.ignite.mem.SimpleNumaAllocationStrategy"/>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</property>
+```
+2. Allocation on specific NUMA node, uses `void *numa_alloc_onnode(size_t, int)` under the hood:
+```xml
+<property name="dataStorageConfiguration">
+    <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
+        <property name="defaultDataRegionConfiguration">
+            <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
+                <property name="name" value="Default_Region"/>
+                ....
+                <property name="memoryAllocator">
+                    <bean class="org.apache.ignite.mem.NumaAllocator">
+                        <constructor-arg>
+                            <bean class="org.apache.ignite.mem.SimpleNumaAllocationStrategy">
+                                <constructor-arg name="node" value="0"/>
+                            </bean>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</property>
+```
+### Interleaved allocation strategy.
+1. Interleaved allocation on all NUMA nodes, uses `void *numa_alloc_interleaved(size_t)` under the hood:
+```xml
+<property name="dataStorageConfiguration">
+    <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
+        <property name="defaultDataRegionConfiguration">
+            <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
+                <property name="name" value="Default_Region"/>
+                ....
+                <property name="memoryAllocator">
+                    <bean class="org.apache.ignite.mem.NumaAllocator">
+                        <constructor-arg>
+                            <bean class="org.apache.ignite.mem.InterleavedNumaAllocationStrategy"/>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</property>
+```
+2. Interleaved allocation on specified NUMA nodes, uses `void *numa_alloc_interleaved_subset(size_t, struct bitmask*)`
+under the hood:
+```xml
+<property name="dataStorageConfiguration">
+    <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
+        <property name="defaultDataRegionConfiguration">
+            <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
+                <property name="name" value="Default_Region"/>
+                ....
+                <property name="memoryAllocator">
+                    <bean class="org.apache.ignite.mem.NumaAllocator">
+                        <constructor-arg>
+                            <bean class="org.apache.ignite.mem.InterleavedNumaAllocationStrategy">
+                                <constructor-arg name="nodes">
+                                    <array>
+                                        <value>0</value>
+                                        <value>1</value>
+                                    </array>
+                                </constructor-arg>
+                            </bean>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</property>
+```
+## Local node allocation strategy
+Allocation on local for process NUMA node, uses `void* numa_alloc_onnode(size_t)` under the hood.
+```xml
+<property name="dataStorageConfiguration">
+    <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
+        <property name="defaultDataRegionConfiguration">
+            <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
+                <property name="name" value="Default_Region"/>
+                ....
+                <property name="memoryAllocator">
+                    <bean class="org.apache.ignite.mem.NumaAllocator">
+                        <constructor-arg>
+                            <constructor-arg>
+                                <bean class="org.apache.ignite.mem.LocalNumaAllocationStrategy"/>
+                            </constructor-arg>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</property>
+```
diff --git a/modules/numa-allocator/pom.xml b/modules/numa-allocator/pom.xml
new file mode 100644
index 0000000..830330a
--- /dev/null
+++ b/modules/numa-allocator/pom.xml
@@ -0,0 +1,210 @@
+<?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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-numa-allocator</artifactId>
+    <version>${revision}</version>
+    <url>http://ignite.apache.org</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>ignite-tools</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-beans</artifactId>
+            <version>${spring.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>${spring.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.thoughtworks.xstream</groupId>
+            <artifactId>xstream</artifactId>
+            <version>1.4.8</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>linux-build</id>
+            <activation>
+                <os>
+                    <name>Linux</name>
+                    <arch>amd64</arch>
+                </os>
+            </activation>
+            <properties>
+                <cmake.classifier>linux-x86_64</cmake.classifier>
+            </properties>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.googlecode.cmake-maven-project</groupId>
+                        <artifactId>cmake-maven-plugin</artifactId>
+                        <version>3.7.2-b1</version>
+                        <executions>
+                            <execution>
+                                <id>cmake-generate</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>generate</goal>
+                                </goals>
+                                <configuration>
+                                    <classifier>${cmake.classifier}</classifier>
+                                    <sourcePath>${basedir}/src/main/cpp</sourcePath>
+                                    <targetPath>${project.build.directory}/cppbuild</targetPath>
+                                    <generator>Unix Makefiles</generator>
+                                    <options>
+                                        <option>-DCMAKE_BUILD_TYPE=Release</option>
+                                    </options>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>cmake-build</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>compile</goal>
+                                </goals>
+                                <configuration>
+                                    <classifier>${cmake.classifier}</classifier>
+                                    <projectDirectory>${project.build.directory}/cppbuild</projectDirectory>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-resources-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>copy-sources</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${project.build.directory}/classes/org/apache/ignite/internal/mem/linux/${os.arch}/</outputDirectory>
+                                    <resources>
+                                        <resource>
+                                            <directory>${project.build.directory}/cppbuild</directory>
+                                            <includes>
+                                                <include>*.so</include>
+                                            </includes>
+                                        </resource>
+                                    </resources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-libs</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <excludeTransitive>false</excludeTransitive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <version>2.8.2</version>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/modules/numa-allocator/src/main/cpp/CMakeLists.txt b/modules/numa-allocator/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..48b42fa
--- /dev/null
+++ b/modules/numa-allocator/src/main/cpp/CMakeLists.txt
@@ -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.
+#
+
+cmake_minimum_required(VERSION 3.6)
+project(numa_alloc)
+
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_BUILD_TYPE Release)
+set(CMAKE_CXX_FLAGS "-shared -D_FORTIFY_SOURCE=2 -z noexecstack -z,relro -z,now -fPIC -Wformat -Wformat-security -Werror=format-security")
+set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pie")
+
+find_package(JNI REQUIRED)
+find_package(Threads REQUIRED)
+include_directories(${CMAKE_THREAD_LIBS_INIT})
+
+find_library(NUMA_LIBRARIES NAMES libnuma.so)
+if (NOT NUMA_LIBRARIES)
+    message(FATAL_ERROR "not found numa library")
+endif (NOT NUMA_LIBRARIES)
+
+include_directories(${JAVA_INCLUDE_PATH})
+include_directories(${JAVA_INCLUDE_PATH2})
+include_directories(include)
+
+file(GLOB SOURCES
+        src/numa/numa_alloc.cpp
+        src/org_apache_ignite_internal_mem_NumaAllocUtil.cpp
+)
+add_library(numa_alloc SHARED ${SOURCES})
+message(STATUS "NUMA_LIBRARIES: ${NUMA_LIBRARIES}")
+target_link_libraries(numa_alloc PRIVATE -static-libgcc -static-libstdc++ ${NUMA_LIBRARIES})
diff --git a/modules/numa-allocator/src/main/cpp/include/numa/numa_alloc.h b/modules/numa-allocator/src/main/cpp/include/numa/numa_alloc.h
new file mode 100644
index 0000000..b116a40
--- /dev/null
+++ b/modules/numa-allocator/src/main/cpp/include/numa/numa_alloc.h
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+#ifndef _NUMA_ALLOC_H
+#define _NUMA_ALLOC_H
+
+#include <ostream>
+
+namespace numa {
+    class BitSet {
+    public:
+        BitSet();
+
+        BitSet(const BitSet &other);
+
+        BitSet &operator=(const BitSet &other);
+
+        ~BitSet();
+
+        class Reference {
+        public:
+            explicit Reference(BitSet &bitset, size_t pos) : bitset(&bitset), pos(pos) {}
+
+            Reference &operator=(bool value) {
+                bitset->Set(pos, value);
+                return *this;
+            }
+
+            Reference &operator=(const Reference &other) {
+                if (&other != this) {
+                    bitset->Set(pos, other.bitset->Get(other.pos));
+                }
+                return *this;
+            }
+
+            explicit operator bool() const {
+                return bitset->Get(pos);
+            }
+
+        private:
+            BitSet *bitset;
+            size_t pos;
+        };
+
+        Reference operator[](size_t pos) {
+            return Reference(*this, pos);
+        }
+
+        bool operator[](size_t pos) const {
+            return this->Get(pos);
+        }
+
+        bool operator==(const BitSet &other);
+
+        bool operator!=(const BitSet &other);
+
+        friend void *AllocInterleaved(size_t size, const BitSet &node_set);
+
+        void Set(size_t pos, bool value = true);
+
+        void Set();
+
+        void Reset(size_t pos);
+
+        void Reset();
+
+        size_t Size() const;
+
+        friend std::ostream &operator<<(std::ostream &os, const BitSet &set);
+
+    private:
+        bool Get(size_t pos) const;
+
+        class BitSetImpl;
+
+        BitSetImpl *p_impl_;
+    };
+
+    int NumaNodesCount();
+
+    void *Alloc(size_t size);
+
+    void *Alloc(size_t size, int node);
+
+    void *AllocLocal(size_t size);
+
+    void *AllocInterleaved(size_t size);
+
+    void *AllocInterleaved(size_t size, const BitSet &node_set);
+
+    size_t Size(void *ptr);
+
+    void Free(void *ptr);
+}
+
+#endif //_NUMA_ALLOC_H
diff --git a/modules/numa-allocator/src/main/cpp/include/org_apache_ignite_internal_mem_NumaAllocUtil.h b/modules/numa-allocator/src/main/cpp/include/org_apache_ignite_internal_mem_NumaAllocUtil.h
new file mode 100644
index 0000000..3a7bbb5
--- /dev/null
+++ b/modules/numa-allocator/src/main/cpp/include/org_apache_ignite_internal_mem_NumaAllocUtil.h
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+#ifndef _ORG_APACHE_IGNITE_INTERNAL_MEM_NUMAALLOCUTIL_H
+#define _ORG_APACHE_IGNITE_INTERNAL_MEM_NUMAALLOCUTIL_H
+
+#include <jni.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocate(JNIEnv *, jclass, jlong);
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateOnNode(JNIEnv *, jclass, jlong, jint);
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateLocal(JNIEnv *, jclass, jlong);
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateInterleaved(JNIEnv *, jclass, jlong, jintArray);
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_chunkSize(JNIEnv *, jclass, jlong);
+JNIEXPORT void JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_free(JNIEnv *, jclass, jlong);
+JNIEXPORT jint JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_nodesCount(JNIEnv *, jclass);
+
+#ifdef __cplusplus
+}
+#endif
+#endif // _ORG_APACHE_IGNITE_INTERNAL_MEM_NUMAALLOCUTIL_H
diff --git a/modules/numa-allocator/src/main/cpp/src/numa/numa_alloc.cpp b/modules/numa-allocator/src/main/cpp/src/numa/numa_alloc.cpp
new file mode 100644
index 0000000..4194978
--- /dev/null
+++ b/modules/numa-allocator/src/main/cpp/src/numa/numa_alloc.cpp
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+#include <numa.h>
+#include <numa/numa_alloc.h>
+
+namespace numa {
+    class BitSet::BitSetImpl {
+    public:
+        BitSetImpl() : size_(numa_max_node() + 1) {
+            mask_ = numa_bitmask_alloc(size_);
+        }
+
+        BitSetImpl(const BitSetImpl &other) : size_(other.size_) {
+            mask_ = numa_bitmask_alloc(size_);
+            copy_bitmask_to_bitmask(other.mask_, mask_);
+        }
+
+        void SetBit(size_t idx, bool val) {
+            if (idx < size_) {
+                if (val)
+                    numa_bitmask_setbit(mask_, idx);
+                else
+                    numa_bitmask_clearbit(mask_, idx);
+            }
+        }
+
+        void SetAll(bool val) {
+            if (val)
+                numa_bitmask_setall(mask_);
+            else
+                numa_bitmask_clearall(mask_);
+        }
+
+        bool GetBit(size_t idx) const {
+            if (idx < size_)
+                return numa_bitmask_isbitset(mask_, idx);
+            else
+                return false;
+        }
+
+        bool Equals(const BitSetImpl &other) const {
+            return numa_bitmask_equal(mask_, other.mask_);
+        }
+
+        size_t Size() const {
+            return size_;
+        }
+
+        bitmask *Raw() {
+            return mask_;
+        }
+
+        ~BitSetImpl() {
+            numa_bitmask_free(mask_);
+        }
+
+    private:
+        bitmask *mask_;
+        size_t size_;
+    };
+
+    BitSet::BitSet() {
+        p_impl_ = new BitSetImpl();
+    }
+
+    BitSet::BitSet(const BitSet &other) {
+        p_impl_ = new BitSetImpl(*other.p_impl_);
+    }
+
+    BitSet &BitSet::operator=(const BitSet &other) {
+        if (this != &other) {
+            BitSet tmp(other);
+            std::swap(this->p_impl_, tmp.p_impl_);
+        }
+        return *this;
+    }
+
+    bool BitSet::Get(size_t pos) const {
+        return p_impl_->GetBit(pos);
+    }
+
+    void BitSet::Set(size_t pos, bool value) {
+        p_impl_->SetBit(pos, value);
+    }
+
+    void BitSet::Set() {
+        p_impl_->SetAll(true);
+    }
+
+    void BitSet::Reset(size_t pos) {
+        p_impl_->SetBit(pos, false);
+    }
+
+    void BitSet::Reset() {
+        p_impl_->SetAll(false);
+    }
+
+    size_t BitSet::Size() const {
+        return p_impl_->Size();
+    }
+
+    bool BitSet::operator==(const BitSet &other) {
+        return this->p_impl_->Equals(*other.p_impl_);
+    }
+
+    bool BitSet::operator!=(const BitSet &other) {
+        return !(*this == other);
+    }
+
+    BitSet::~BitSet() {
+        delete p_impl_;
+    }
+
+    std::ostream &operator<<(std::ostream &os, const BitSet &set) {
+        os << '{';
+        for (size_t i = 0; i < set.Size(); ++i) {
+            os << set[i];
+            if (i < set.Size() - 1) {
+                os << ", ";
+            }
+        }
+        os << '}';
+        return os;
+    }
+
+    int NumaNodesCount() {
+        return numa_max_node() + 1;
+    }
+
+    /**
+     *  Memory layout:
+     *  +-------------------------------+------------------------+
+     *  | Header (sizeof(max_align_t))  |  Application memory    |
+     *  +------------------------------ +------------------------+
+     *                                  ^
+     *                                  |
+     *                          Result pointer
+     * Size of application memory chunk is written to header.
+     * Total allocated size equals to size of application memory chunk plus sizeof(max_align_t).
+     */
+    union region_size {
+        size_t size;
+        max_align_t a;
+    };
+
+    template<typename Func, typename ...Args>
+    inline void *NumaAllocHelper(Func f, size_t size, Args ...args) {
+        auto ptr = static_cast<region_size *>(f(size + sizeof(region_size), args...));
+        if (ptr) {
+            ptr->size = size;
+            ptr++;
+        }
+        return ptr;
+    }
+
+    inline region_size* ConvertPointer(void* buf) {
+        if (buf) {
+            auto *ptr = static_cast<region_size *>(buf);
+            ptr--;
+            return ptr;
+        }
+        return nullptr;
+    }
+
+    void *Alloc(size_t size) {
+        return NumaAllocHelper(numa_alloc, size);
+    }
+
+    void *Alloc(size_t size, int node) {
+        return NumaAllocHelper(numa_alloc_onnode, size, node);
+    }
+
+    void *AllocLocal(size_t size) {
+        return NumaAllocHelper(numa_alloc_local, size);
+    }
+
+    void *AllocInterleaved(size_t size) {
+        return NumaAllocHelper(numa_alloc_interleaved, size);
+    }
+
+    void *AllocInterleaved(size_t size, const BitSet &node_set) {
+        return NumaAllocHelper(numa_alloc_interleaved_subset, size, node_set.p_impl_->Raw());
+    }
+
+    size_t Size(void *buf) {
+        auto ptr = ConvertPointer(buf);
+        if (ptr) {
+            return ptr->size;
+        }
+        return 0;
+    }
+
+    void Free(void *buf) {
+        auto ptr = ConvertPointer(buf);
+        if (ptr) {
+            numa_free(ptr, ptr->size + sizeof(region_size));
+        }
+    }
+}
diff --git a/modules/numa-allocator/src/main/cpp/src/org_apache_ignite_internal_mem_NumaAllocUtil.cpp b/modules/numa-allocator/src/main/cpp/src/org_apache_ignite_internal_mem_NumaAllocUtil.cpp
new file mode 100644
index 0000000..38ad95b
--- /dev/null
+++ b/modules/numa-allocator/src/main/cpp/src/org_apache_ignite_internal_mem_NumaAllocUtil.cpp
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+
+#include <numa/numa_alloc.h>
+#include "org_apache_ignite_internal_mem_NumaAllocUtil.h"
+
+class JIntArrayGuard {
+public:
+    JIntArrayGuard(JNIEnv* env, jintArray arr): env_(env), java_arr_(arr) {
+        arr_ = env->GetIntArrayElements(java_arr_, nullptr);
+        length_ = static_cast<size_t>(env->GetArrayLength(java_arr_));
+    }
+
+    JIntArrayGuard() = delete;
+    JIntArrayGuard(const JIntArrayGuard&) = delete;
+    JIntArrayGuard& operator=(const JIntArrayGuard&) = delete;
+
+    ~JIntArrayGuard() {
+        env_->ReleaseIntArrayElements(java_arr_, arr_, 0);
+    }
+
+    const int& operator[](size_t pos) const {
+        return arr_[pos];
+    }
+
+    int& operator[](size_t pos) {
+        return arr_[pos];
+    }
+
+    size_t Size() const {
+        return length_;
+    }
+private:
+    JNIEnv* env_;
+    jintArray java_arr_;
+    jint* arr_;
+    size_t length_;
+};
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocate(
+        JNIEnv*,
+        jclass,
+        jlong size
+) {
+    void* ptr = numa::Alloc(static_cast<size_t>(size));
+    return reinterpret_cast<jlong>(ptr);
+}
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateOnNode(
+        JNIEnv*,
+        jclass,
+        jlong size,
+        jint node
+) {
+    void* ptr;
+    auto size_ = static_cast<size_t>(size);
+    if (node >= 0 && node < numa::NumaNodesCount()) {
+        ptr = numa::Alloc(size_, static_cast<int>(node));
+    }
+    else {
+        ptr = numa::Alloc(size_);
+    }
+    return reinterpret_cast<jlong>(ptr);
+}
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateLocal(
+        JNIEnv*,
+        jclass,
+        jlong size
+) {
+    void* ptr = numa::AllocLocal(static_cast<size_t>(size));
+    return reinterpret_cast<jlong>(ptr);
+}
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_allocateInterleaved(
+        JNIEnv *jniEnv,
+        jclass,
+        jlong size,
+        jintArray arr
+) {
+    void* ptr;
+    auto size_ = static_cast<size_t>(size);
+    if (arr != nullptr) {
+        JIntArrayGuard nodes(jniEnv, arr);
+
+        if (nodes.Size() > 0) {
+            numa::BitSet bs;
+            for (size_t i = 0; i < nodes.Size(); ++i) {
+                bs.Set(nodes[i]);
+            }
+            ptr = numa::AllocInterleaved(size_, bs);
+        }
+        else {
+            ptr = numa::AllocInterleaved(size_);
+        }
+    }
+    else {
+        ptr = numa::AllocInterleaved(size_);
+    }
+    return reinterpret_cast<jlong>(ptr);
+}
+
+JNIEXPORT jlong JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_chunkSize(JNIEnv *, jclass, jlong addr) {
+    void* ptr = reinterpret_cast<void*>(addr);
+    return static_cast<jlong>(numa::Size(ptr));
+}
+
+JNIEXPORT void JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_free(JNIEnv *, jclass, jlong addr) {
+    void* ptr = reinterpret_cast<void*>(addr);
+    numa::Free(ptr);
+}
+
+JNIEXPORT jint JNICALL Java_org_apache_ignite_internal_mem_NumaAllocUtil_nodesCount(JNIEnv *, jclass) {
+    return static_cast<jint>(numa::NumaNodesCount());
+}
diff --git a/modules/numa-allocator/src/main/java/org/apache/ignite/internal/mem/NumaAllocUtil.java b/modules/numa-allocator/src/main/java/org/apache/ignite/internal/mem/NumaAllocUtil.java
new file mode 100644
index 0000000..18f753f
--- /dev/null
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/internal/mem/NumaAllocUtil.java
@@ -0,0 +1,151 @@
+/*
+ * 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.mem;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Paths;
+
+/** */
+public class NumaAllocUtil {
+    /** */
+    private static final String extension = ".so";
+
+    /** */
+    private static final String libraryName = "libnuma_alloc";
+
+    /** */
+    private NumaAllocUtil() {
+        // No-op.
+    }
+
+    /** */
+    private static String getLibName() {
+        return Paths.get("/org/apache/ignite/internal/mem", getOSName(), getOSArch(), libraryName + extension)
+            .toString();
+    }
+
+    /** */
+    private static String getOSArch() {
+        return System.getProperty("os.arch", "");
+    }
+
+    /** */
+    private static String getOSName() {
+        if (System.getProperty("os.name", "").contains("Linux"))
+            return "linux";
+        else
+            throw new UnsupportedOperationException("Operating System is not supported");
+    }
+
+    static {
+        String libName = getLibName();
+        File nativeLib = null;
+
+        try (InputStream in = NumaAllocUtil.class.getResourceAsStream(libName)) {
+            if (in == null) {
+                throw new ExceptionInInitializerError("Failed to load native numa_alloc library, "
+                    + libName + " not found");
+            }
+
+            nativeLib = File.createTempFile(libraryName, extension);
+
+            try (FileOutputStream out = new FileOutputStream(nativeLib)) {
+                byte[] buf = new byte[4096];
+
+                int bytesRead;
+                while ((bytesRead = in.read(buf)) > 0)
+                    out.write(buf, 0, bytesRead);
+            }
+
+            System.load(nativeLib.getAbsolutePath());
+
+            NUMA_NODES_CNT = nodesCount();
+        }
+        catch (IOException | UnsatisfiedLinkError e) {
+            throw new ExceptionInInitializerError(e);
+        }
+        finally {
+            if (nativeLib != null)
+                nativeLib.deleteOnExit();
+        }
+    }
+
+    /** */
+    public static final int NUMA_NODES_CNT;
+
+    /**
+     * Allocate memory using using default NUMA memory policy of current thread.
+     * Uses {@code void *numa_alloc(size_t)} under the hood.
+     * <p>
+     * @param size Size of buffer.
+     * @return Address of buffer.
+     */
+    public static native long allocate(long size);
+
+    /**
+     * Allocate memory on specific NUMA node. Uses {@code void *numa_alloc_onnode(size_t)} under the hood.
+     * <p>
+     * @param size Size of buffer.
+     * @param node NUMA node.
+     * @return Address of buffer.
+     */
+    public static native long allocateOnNode(long size, int node);
+
+    /**
+     * Allocate memory on local NUMA node. Uses {@code void *numa_alloc_local(size_t)} under the hood.
+     * <p>
+     * @param size Size of buffer.
+     * @return Address of buffer.
+     */
+    public static native long allocateLocal(long size);
+
+    /**
+     * Allocate memory interleaved on NUMA nodes. Uses
+     * {@code void *numa_alloc_interleaved_subset(size_t, struct bitmask*)} under the hood.
+     * <p>
+     * @param size Size of buffer.
+     * @param nodes Array of nodes allocate on. If {@code null} or empty, allocate on all NODES.
+     * @return Address of buffer.
+     */
+    public static native long allocateInterleaved(long size, int[] nodes);
+
+    /**
+     * Get allocated buffer size.
+     *
+     * @param addr Address of buffer.
+     * @return Size of buffer.
+     */
+    public static native long chunkSize(long addr);
+
+    /**
+     * Free allocated memory.
+     *
+     * @param addr Address of buffer.
+     */
+    public static native void free(long addr);
+
+    /**
+     * Get NUMA nodes count.
+     *
+     * @return NUMA nodes count available on system.
+     */
+    private static native int nodesCount();
+}
diff --git a/modules/numa-allocator/src/main/java/org/apache/ignite/mem/InterleavedNumaAllocationStrategy.java b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/InterleavedNumaAllocationStrategy.java
new file mode 100644
index 0000000..573078c
--- /dev/null
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/InterleavedNumaAllocationStrategy.java
@@ -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.
+ */
+
+package org.apache.ignite.mem;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import org.apache.ignite.internal.mem.NumaAllocUtil;
+import org.apache.ignite.internal.util.tostring.GridToStringBuilder;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.A;
+
+/**
+ * Interleaved NUMA allocation strategy.
+ * <p>
+ * Use {@link InterleavedNumaAllocationStrategy#InterleavedNumaAllocationStrategy()} to allocate memory interleaved
+ * on all available NUMA nodes. Memory will be allocated using {@code void *numa_alloc_interleaved(size_t)}
+ * of {@code libnuma}.
+ * <p>
+ * Use {@link InterleavedNumaAllocationStrategy#InterleavedNumaAllocationStrategy(int[])} to allocate memory interleaved
+ * on specified nodes.
+ * {@code void *numa_alloc_interleaved_subset(size_t, struct bitmask*)} of {@code lubnuma}.
+ */
+public class InterleavedNumaAllocationStrategy implements NumaAllocationStrategy, Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    @GridToStringInclude
+    private final int[] nodes;
+
+    /** */
+    public InterleavedNumaAllocationStrategy() {
+        this(null);
+    }
+
+    /**
+     * @param nodes Array of NUMA nodes to allocate on.
+     */
+    public InterleavedNumaAllocationStrategy(int[] nodes) {
+        if (nodes != null && nodes.length > 0) {
+            this.nodes = Arrays.copyOf(nodes, nodes.length);
+
+            Arrays.sort(this.nodes);
+            A.ensure(this.nodes[0] >= 0, "NUMA node number must be positive, passed instead "
+                + Arrays.toString(this.nodes));
+            A.ensure(this.nodes[this.nodes.length - 1] < NumaAllocUtil.NUMA_NODES_CNT,
+                "NUMA node number must be less than NUMA_NODES_CNT=" + NumaAllocUtil.NUMA_NODES_CNT +
+                    ", passed instead " + Arrays.toString(this.nodes));
+        }
+        else
+            this.nodes = null;
+    }
+
+    /** {@inheritDoc}*/
+    @Override public long allocateMemory(long size) {
+        return NumaAllocUtil.allocateInterleaved(size, nodes);
+    }
+
+    /** {@inheritDoc}*/
+    @Override public String toString() {
+        return GridToStringBuilder.toString(InterleavedNumaAllocationStrategy.class, this);
+    }
+}
diff --git a/modules/numa-allocator/src/main/java/org/apache/ignite/mem/LocalNumaAllocationStrategy.java b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/LocalNumaAllocationStrategy.java
new file mode 100644
index 0000000..e4e3387
--- /dev/null
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/LocalNumaAllocationStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * 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.mem;
+
+import java.io.Serializable;
+import org.apache.ignite.internal.mem.NumaAllocUtil;
+import org.apache.ignite.internal.util.tostring.GridToStringBuilder;
+
+/**
+ * Local NUMA allocation strategy.
+ * <p>
+ * Memory will be allocated using {@code void* numa_alloc_onnode(size_t)} of {@code lubnuma}.
+ */
+public class LocalNumaAllocationStrategy implements NumaAllocationStrategy, Serializable {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override public long allocateMemory(long size) {
+        return NumaAllocUtil.allocateLocal(size);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return GridToStringBuilder.toString(LocalNumaAllocationStrategy.class, this);
+    }
+}
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocationStrategy.java
similarity index 71%
copy from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
copy to modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocationStrategy.java
index a5b6d8f..781bf71 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocationStrategy.java
@@ -15,18 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+package org.apache.ignite.mem;
 
 /**
- * Zookeeper IP Finder tests.
+ * Basic interface for allocating memory on NUMA nodes with different policies using {@code libnuma}.
  */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
-})
-public class ZookeeperIpFinderTestSuite {
-
+public interface NumaAllocationStrategy {
+    /**
+     * @param size Size of allocated memory.
+     *
+     * @return Pointer to memory.
+     */
+    public long allocateMemory(long size);
 }
diff --git a/modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocator.java b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocator.java
new file mode 100644
index 0000000..8b90862
--- /dev/null
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.mem;
+
+import java.io.Serializable;
+import org.apache.ignite.internal.mem.NumaAllocUtil;
+
+/**
+ * NUMA aware memory allocator. Uses {@code libnuma} under the hood. Only Linux distros with {@code libnuma >= 2.0.x}
+ * are supported.
+ * <p>
+ * Allocation strategy can be defined by setting {@code allocStrategy} to
+ * {@link NumaAllocator#NumaAllocator(NumaAllocationStrategy)}.
+ */
+public class NumaAllocator implements MemoryAllocator, Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    private final NumaAllocationStrategy allocStrategy;
+
+    /**
+     * @param allocStrategy Allocation strategy.
+     */
+    public NumaAllocator(NumaAllocationStrategy allocStrategy) {
+        this.allocStrategy = allocStrategy;
+    }
+
+    /** {@inheritDoc}*/
+    @Override public long allocateMemory(long size) {
+        return allocStrategy.allocateMemory(size);
+    }
+
+    /** {@inheritDoc}*/
+    @Override public void freeMemory(long addr) {
+        NumaAllocUtil.free(addr);
+    }
+}
diff --git a/modules/numa-allocator/src/main/java/org/apache/ignite/mem/SimpleNumaAllocationStrategy.java b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/SimpleNumaAllocationStrategy.java
new file mode 100644
index 0000000..d74fb7a
--- /dev/null
+++ b/modules/numa-allocator/src/main/java/org/apache/ignite/mem/SimpleNumaAllocationStrategy.java
@@ -0,0 +1,77 @@
+/*
+ * 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.mem;
+
+import java.io.Serializable;
+import org.apache.ignite.internal.mem.NumaAllocUtil;
+import org.apache.ignite.internal.util.tostring.GridToStringBuilder;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.A;
+
+/**
+ * Simple NUMA allocation strategy.
+ * <p>
+ * Use {@link SimpleNumaAllocationStrategy#SimpleNumaAllocationStrategy(int)} to allocate memory on specific NUMA node
+ * with number equals to {@code node}. Memory will be allocated using {@code void *numa_alloc_onnode(size_t, int)}
+ * of {@code libnuma}.
+ * <p>
+ * Use {@link SimpleNumaAllocationStrategy#SimpleNumaAllocationStrategy()} to allocate memory using default NUMA
+ * memory policy of current thread. Memory will be allocated using {@code void *numa_alloc(size_t)} of {@code lubnuma}.
+ * Memory policy could be set by running application with {@code numactl}
+ */
+public class SimpleNumaAllocationStrategy implements NumaAllocationStrategy, Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    private static final int NODE_NOT_SET = Integer.MAX_VALUE;
+
+    /** */
+    @GridToStringInclude
+    private final int node;
+
+    /** */
+    public SimpleNumaAllocationStrategy() {
+        node = NODE_NOT_SET;
+    }
+
+    /**
+     * @param node Numa node to allocate on.
+     */
+    public SimpleNumaAllocationStrategy(int node) {
+        A.ensure(node >= 0, "NUMA node number must be positive, passed instead " + node);
+        A.ensure(node < NumaAllocUtil.NUMA_NODES_CNT,
+            "NUMA node number must be less than NUMA_NODES_CNT=" + NumaAllocUtil.NUMA_NODES_CNT +
+                ", passed instead " + node);
+
+        this.node = node;
+    }
+
+    /** {@inheritDoc}*/
+    @Override public long allocateMemory(long size) {
+        if (node == NODE_NOT_SET)
+            return NumaAllocUtil.allocate(size);
+
+        return NumaAllocUtil.allocateOnNode(size, node);
+    }
+
+    /** {@inheritDoc}*/
+    @Override public String toString() {
+        return GridToStringBuilder.toString(SimpleNumaAllocationStrategy.class, this);
+    }
+}
diff --git a/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorBasicTest.java b/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorBasicTest.java
new file mode 100644
index 0000000..db959d9
--- /dev/null
+++ b/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorBasicTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.mem;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.mem.InterleavedNumaAllocationStrategy;
+import org.apache.ignite.mem.LocalNumaAllocationStrategy;
+import org.apache.ignite.mem.NumaAllocationStrategy;
+import org.apache.ignite.mem.NumaAllocator;
+import org.apache.ignite.mem.SimpleNumaAllocationStrategy;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/** */
+@RunWith(Parameterized.class)
+public class NumaAllocatorBasicTest extends GridCommonAbstractTest {
+    /** */
+    private static final long INITIAL_SIZE = 30L * 1024 * 1024;
+
+    /** */
+    private static final long MAX_SIZE = 100L * 1024 * 1024;
+
+    /** */
+    private static final String TEST_CACHE = "test";
+
+    /** */
+    private static final byte[] BUF = new byte[4096];
+
+    /** */
+    private static final int NUM_NODES = 3;
+
+    static {
+        ThreadLocalRandom.current().nextBytes(BUF);
+    }
+
+    /** */
+    @Parameterized.Parameters(name = "allocationStrategy={0}, defaultConfig={1}")
+    public static Iterable<Object[]> data() {
+        return Stream.of(
+            new LocalNumaAllocationStrategy(),
+            new InterleavedNumaAllocationStrategy(),
+            new InterleavedNumaAllocationStrategy(IntStream.range(0, NumaAllocUtil.NUMA_NODES_CNT).toArray()),
+            new SimpleNumaAllocationStrategy(),
+            new SimpleNumaAllocationStrategy(NumaAllocUtil.NUMA_NODES_CNT - 1)
+            )
+            .flatMap(strategy -> Stream.of(new Object[]{strategy, true}, new Object[]{strategy, false}))
+            .collect(Collectors.toList());
+    }
+
+    /** */
+    @Parameterized.Parameter(0)
+    public NumaAllocationStrategy strategy;
+
+    /** */
+    @Parameterized.Parameter(1)
+    public boolean defaultConfig;
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        DataStorageConfiguration memCfg = new DataStorageConfiguration();
+
+        DataRegionConfiguration dfltReg = new DataRegionConfiguration()
+            .setInitialSize(INITIAL_SIZE)
+            .setMaxSize(MAX_SIZE)
+            .setMetricsEnabled(true);
+
+        NumaAllocator memAlloc = new NumaAllocator(strategy);
+
+        if (defaultConfig)
+            memCfg.setMemoryAllocator(memAlloc);
+        else
+            dfltReg.setMemoryAllocator(memAlloc);
+
+        memCfg.setDefaultDataRegionConfiguration(dfltReg);
+
+        cfg.setDataStorageConfiguration(memCfg);
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        startGrids(NUM_NODES);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        stopAllGrids(true);
+    }
+
+    /** */
+    @Test
+    public void testLoadData() throws Exception {
+        IgniteEx client = startClientGrid("client");
+
+        client.getOrCreateCache(TEST_CACHE);
+
+        try (IgniteDataStreamer<Integer, byte[]> ds = client.dataStreamer(TEST_CACHE)) {
+            int cnt = 0;
+            while (hasFreeSpace()) {
+                ds.addData(++cnt, BUF);
+
+                if (cnt % 100 == 0)
+                    ds.flush();
+            }
+        }
+
+        assertEquals(NUM_NODES, serverGrids().count());
+
+        serverGrids().forEach(g -> {
+            assertTrue(getDefaultRegion(g).config().getMemoryAllocator() instanceof NumaAllocator);
+        });
+    }
+
+    /** */
+    private boolean hasFreeSpace() {
+       return serverGrids().allMatch(g -> {
+            DataRegion dr = getDefaultRegion(g);
+
+            return dr.metrics().getTotalAllocatedSize() < 0.9 * MAX_SIZE;
+        });
+    }
+
+    /** */
+    private static Stream<IgniteEx> serverGrids() {
+        return G.allGrids().stream().filter(g -> !g.cluster().localNode().isClient()).map(g -> (IgniteEx)g);
+    }
+
+    /** */
+    private static DataRegion getDefaultRegion(IgniteEx g) {
+        assertFalse(g.cluster().localNode().isClient());
+
+        String dataRegionName = g.configuration().getDataStorageConfiguration()
+            .getDefaultDataRegionConfiguration().getName();
+
+        try {
+            return g.context().cache().context().database().dataRegion(dataRegionName);
+        }
+        catch (IgniteCheckedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorUnitTest.java b/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorUnitTest.java
new file mode 100644
index 0000000..7044ea8
--- /dev/null
+++ b/modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem/NumaAllocatorUnitTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.mem;
+
+import java.util.Arrays;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.util.GridUnsafe;
+import org.apache.ignite.mem.InterleavedNumaAllocationStrategy;
+import org.apache.ignite.mem.LocalNumaAllocationStrategy;
+import org.apache.ignite.mem.NumaAllocationStrategy;
+import org.apache.ignite.mem.NumaAllocator;
+import org.apache.ignite.mem.SimpleNumaAllocationStrategy;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/** */
+@RunWith(Enclosed.class)
+public class NumaAllocatorUnitTest {
+    /** */
+    @RunWith(Parameterized.class)
+    public static class PositiveScenarioTest extends GridCommonAbstractTest {
+        /** */
+        private static final long BUF_SZ = 32 * 1024 * 1024;
+
+        /** */
+        private static final int[] EVEN_NODES = IntStream.range(0, NumaAllocUtil.NUMA_NODES_CNT)
+            .filter(x -> x % 2 == 0).toArray();
+
+        /** */
+        private static final int[] ALL_NODES = IntStream.range(0, NumaAllocUtil.NUMA_NODES_CNT).toArray();
+
+        /**
+         *
+         */
+        @Parameterized.Parameters(name = "allocationStrategy={0}")
+        public static Iterable<Object[]> data() {
+            return Arrays.asList(
+                new Object[] {new LocalNumaAllocationStrategy()},
+                new Object[] {new InterleavedNumaAllocationStrategy()},
+                new Object[] {new InterleavedNumaAllocationStrategy(new int[0])},
+                new Object[] {new InterleavedNumaAllocationStrategy(EVEN_NODES)},
+                new Object[] {new InterleavedNumaAllocationStrategy(ALL_NODES)},
+                new Object[] {new SimpleNumaAllocationStrategy()},
+                new Object[] {new SimpleNumaAllocationStrategy(NumaAllocUtil.NUMA_NODES_CNT - 1)}
+            );
+        }
+
+        /** */
+        @Parameterized.Parameter()
+        public NumaAllocationStrategy strategy;
+
+        /** */
+        @Test
+        public void test() {
+            NumaAllocator allocator = new NumaAllocator(strategy);
+
+            long ptr = 0;
+            try {
+                ptr = allocator.allocateMemory(BUF_SZ);
+
+                assertEquals(BUF_SZ, NumaAllocUtil.chunkSize(ptr));
+
+                GridUnsafe.setMemory(ptr, BUF_SZ, (byte)1);
+
+                for (long i = 0; i < BUF_SZ; i++)
+                    assertEquals((byte)1, GridUnsafe.getByte(ptr + i));
+            }
+            finally {
+                if (ptr != 0)
+                    allocator.freeMemory(ptr);
+            }
+        }
+    }
+
+    /** */
+    public static class ErrorScenarioTest extends GridCommonAbstractTest {
+        /** */
+        @Test
+        public void testInvalidInterleavedStrategyParams() {
+            int[][] invalidNodes = {
+                {-3, -4, 0},
+                IntStream.range(0, NumaAllocUtil.NUMA_NODES_CNT + 1).toArray()
+            };
+
+            for (int[] nodeSet: invalidNodes) {
+                GridTestUtils.assertThrows(log(), () -> new InterleavedNumaAllocationStrategy(nodeSet),
+                    IllegalArgumentException.class, null);
+            }
+        }
+
+        /** */
+        @Test
+        public void testInvalidSimpleStrategyParams() {
+            int[] invalidNodes = {-3, NumaAllocUtil.NUMA_NODES_CNT};
+
+            for (int node: invalidNodes) {
+                GridTestUtils.assertThrows(log(), () -> new SimpleNumaAllocationStrategy(node),
+                    IllegalArgumentException.class, null);
+            }
+        }
+    }
+}
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java b/modules/numa-allocator/src/test/java/org/apache/ignite/testsuites/NumaAllocatorTestSuite.java
similarity index 76%
rename from modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
rename to modules/numa-allocator/src/test/java/org/apache/ignite/testsuites/NumaAllocatorTestSuite.java
index a5b6d8f..9fc6ab6 100644
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTestSuite.java
+++ b/modules/numa-allocator/src/test/java/org/apache/ignite/testsuites/NumaAllocatorTestSuite.java
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
+package org.apache.ignite.testsuites;
 
+import org.apache.ignite.internal.mem.NumaAllocatorBasicTest;
+import org.apache.ignite.internal.mem.NumaAllocatorUnitTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
-/**
- * Zookeeper IP Finder tests.
- */
+/** */
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
-    ZookeeperIpFinderTest.class
+    NumaAllocatorUnitTest.class,
+    NumaAllocatorBasicTest.class
 })
-public class ZookeeperIpFinderTestSuite {
-
+public class NumaAllocatorTestSuite {
 }
diff --git a/modules/platforms/cpp/CMakeLists.txt b/modules/platforms/cpp/CMakeLists.txt
index 2c35dc3..a9b88a6 100644
--- a/modules/platforms/cpp/CMakeLists.txt
+++ b/modules/platforms/cpp/CMakeLists.txt
@@ -49,12 +49,31 @@
     add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS)
 endif()
 
-option (WITH_CORE ON)
-option (WITH_ODBC OFF)
-option (WITH_ODBC_MSI OFF)
-option (WITH_THIN_CLIENT OFF)
-option (WITH_TESTS OFF)
-option (WARNINGS_AS_ERRORS OFF)
+option (WITH_CORE "Build Ignite.C++ Core module" ON)
+option (WITH_ODBC "Build Ignite.C++ ODBC driver module" OFF)
+option (WITH_ODBC_MSI "Build Ignite.C++ ODBC driver installer for Windows" OFF)
+option (WITH_THIN_CLIENT "Build Ignite.C++ Thin Client module" OFF)
+option (WITH_TESTS "Build Ignite.C++ tests" OFF)
+option (WARNINGS_AS_ERRORS "Treat warning as errors" OFF)
+option (WITH_SANITIZERS "Build with sanitizers" OFF)
+
+if (${WITH_SANITIZERS} AND (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU"))
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -g")
+
+    if (DEFINED ENV{ASAN_OPTIONS})
+        list(APPEND SANITIZERS_ENV "ASAN_OPTIONS=$ENV{ASAN_OPTIONS}")
+    else()
+        list(APPEND SANITIZERS_ENV "ASAN_OPTIONS=handle_segv=0:detect_leaks=0")
+    endif()
+
+    if (DEFINED ENV{UBSAN_OPTIONS})
+        list(APPEND SANITIZERS_ENV "UBSAN_OPTIONS=$ENV{UBSAN_OPTIONS}")
+    else()
+        list(APPEND SANITIZERS_ENV "UBSAN_OPTIONS=print_stacktrace=1")
+    endif()
+
+    message("Built with sanitizers. Sanitizers options for ctest: ${SANITIZERS_ENV}")
+endif()
 
 if (${WITH_CORE} OR ${WITH_TESTS})
     find_package(Java 1.8 REQUIRED)
diff --git a/modules/platforms/cpp/binary/include/ignite/impl/binary/binary_utils.h b/modules/platforms/cpp/binary/include/ignite/impl/binary/binary_utils.h
index 58e5819..ce79baf 100644
--- a/modules/platforms/cpp/binary/include/ignite/impl/binary/binary_utils.h
+++ b/modules/platforms/cpp/binary/include/ignite/impl/binary/binary_utils.h
@@ -250,7 +250,7 @@
                  * @param pos Position in memory.
                  * @return Value.
                  */
-                static int32_t ReadInt32(interop::InteropMemory& mem, int32_t pos);
+                static int32_t ReadInt32(const interop::InteropMemory& mem, int32_t pos);
 
                 /**
                  * Utility method to read signed 32-bit integer from memory.
@@ -260,7 +260,7 @@
                  * @param pos Position in memory.
                  * @return Value.
                  */
-                static int32_t UnsafeReadInt32(interop::InteropMemory& mem, int32_t pos);
+                static int32_t UnsafeReadInt32(const interop::InteropMemory& mem, int32_t pos);
 
                 /**
                  * Utility method to write signed 32-bit integer to stream.
diff --git a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_input_stream.h b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_input_stream.h
index 3e14171..d48a256 100644
--- a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_input_stream.h
+++ b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_input_stream.h
@@ -27,16 +27,31 @@
         namespace interop
         {
             /**
+             * Helper class.
+             */
+            class InputStreamHelper;
+
+            /**
              * Interop input stream implementation.
              */
-            class IGNITE_IMPORT_EXPORT InteropInputStream {
+            class IGNITE_IMPORT_EXPORT InteropInputStream
+            {
+                friend class InputStreamHelper;
             public:
                 /**
                  * Constructor.
                  *
                  * @param mem Memory.
                  */
-                explicit InteropInputStream(InteropMemory* mem);
+                explicit InteropInputStream(const InteropMemory* mem);
+
+                /**
+                 * Constructor.
+                 *
+                 * @param mem Memory.
+                 * @param len Length. Should be <= mem->Length().
+                 */
+                explicit InteropInputStream(const InteropMemory* mem, int32_t len);
 
                 /**
                  * Read signed 8-byte int.
@@ -51,7 +66,7 @@
                  * @param pos Position.
                  * @return Value.
                  */
-                int32_t ReadInt8(int32_t pos);
+                int8_t ReadInt8(int32_t pos);
 
                 /**
                  * Read signed 8-byte int array.
@@ -89,7 +104,7 @@
                  * @param pos Position.
                  * @return Value.
                  */
-                int32_t ReadInt16(int32_t pos);
+                int16_t ReadInt16(int32_t pos);
 
                 /**
                  * Read signed 16-byte int array.
@@ -219,17 +234,17 @@
                  * Get memory.
                  * @return Underlying memory.
                  */
-                InteropMemory* GetMemory()
+                const InteropMemory* GetMemory()
                 {
                     return mem;
                 }
 
             private:
                 /** Memory. */
-                InteropMemory* mem; 
+                const InteropMemory* mem;
 
                 /** Pointer to data. */
-                int8_t* data;
+                const int8_t* data;
 
                 /** Length. */
                 int len;
@@ -251,7 +266,7 @@
                  * @param off Offset.
                  * @param cnt Amount of data to copy.
                  */
-                void CopyAndShift(int8_t* dest, int32_t off, int32_t cnt);
+                void CopyAndShift(void* dest, int32_t off, int32_t cnt);
 
                 /**
                  * Shift stream to the right.
diff --git a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_memory.h b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_memory.h
index d5c3d60..5982bb4 100644
--- a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_memory.h
+++ b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_memory.h
@@ -21,6 +21,7 @@
 #include <stdint.h>
 
 #include <ignite/common/common.h>
+#include <ignite/common/concurrent.h>
 
 namespace ignite 
 {
@@ -205,7 +206,7 @@
                 /**
                  * Get cross-platform pointer in long form.
                  */
-                int64_t PointerLong();
+                int64_t PointerLong() const;
 
                 /**
                  * Get raw data pointer.
@@ -260,6 +261,9 @@
                 int8_t* memPtr; 
             };
 
+            typedef common::concurrent::SharedPointer<interop::InteropMemory> SP_InteropMemory;
+            typedef common::concurrent::SharedPointer<const interop::InteropMemory> SP_ConstInteropMemory;
+
             /**
              * Interop unpooled memory.
              */
diff --git a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_output_stream.h b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_output_stream.h
index 723412a..3a3fa58 100644
--- a/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_output_stream.h
+++ b/modules/platforms/cpp/binary/include/ignite/impl/interop/interop_output_stream.h
@@ -27,9 +27,16 @@
         namespace interop
         {
             /**
+             * Helper class.
+             */
+            class OutputStreamHelper;
+
+            /**
              * Interop output stream.
              */
-            class IGNITE_IMPORT_EXPORT InteropOutputStream {
+            class IGNITE_IMPORT_EXPORT InteropOutputStream
+            {
+                friend class OutputStreamHelper;
             public:
                 /**
                  * Create new output stream with the given capacity.
@@ -43,14 +50,14 @@
                  *
                  * @param val Value.
                  */
-                void WriteInt8(const int8_t val);
+                void WriteInt8(int8_t val);
 
                 /**
                  * Write signed 8-byte integer at the given position.
                  *
                  * @param val Value.
                  */
-                void WriteInt8(const int8_t val, const int32_t pos);
+                void WriteInt8(int8_t val, int32_t pos);
 
                 /**
                  * Write signed 8-byte integer array.
@@ -58,14 +65,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteInt8Array(const int8_t* val, const int32_t len);
+                void WriteInt8Array(const int8_t* val, int32_t len);
 
                 /**
                  * Write bool.
                  *
                  * @param val Value.
                  */
-                void WriteBool(const bool val);
+                void WriteBool(bool val);
 
                 /**
                  * Write bool array.
@@ -73,14 +80,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteBoolArray(const bool* val, const int32_t len);
+                void WriteBoolArray(const bool* val, int32_t len);
 
                 /**
                  * Write signed 16-byte integer.
                  *
                  * @param val Value.
                  */
-                void WriteInt16(const int16_t val);
+                void WriteInt16(int16_t val);
 
                 /**
                  * Write signed 16-byte integer at the given position.
@@ -88,7 +95,7 @@
                  * @param pos Position.
                  * @param val Value.
                  */
-                void WriteInt16(const int32_t pos, const int16_t val);
+                void WriteInt16(int32_t pos, int16_t val);
 
                 /**
                  * Write signed 16-byte integer array.
@@ -96,14 +103,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteInt16Array(const int16_t* val, const int32_t len);
+                void WriteInt16Array(const int16_t* val, int32_t len);
 
                 /**
                  * Write unsigned 16-byte integer.
                  *
                  * @param val Value.
                  */
-                void WriteUInt16(const uint16_t val);
+                void WriteUInt16(uint16_t val);
 
                 /**
                  * Write unsigned 16-byte integer array.
@@ -111,14 +118,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteUInt16Array(const uint16_t* val, const int32_t len);
+                void WriteUInt16Array(const uint16_t* val, int32_t len);
 
                 /**
                  * Write signed 32-byte integer.
                  *
                  * @param val Value.
                  */
-                void WriteInt32(const int32_t val);
+                void WriteInt32(int32_t val);
 
                 /**
                  * Write signed 32-byte integer at the given position.
@@ -126,7 +133,7 @@
                  * @param pos Position.
                  * @param val Value.
                  */
-                void WriteInt32(const int32_t pos, const int32_t val);
+                void WriteInt32(int32_t pos, int32_t val);
 
                 /**
                  * Write signed 32-byte integer array.
@@ -134,14 +141,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteInt32Array(const int32_t* val, const int32_t len);
+                void WriteInt32Array(const int32_t* val, int32_t len);
 
                 /**
                  * Write signed 64-byte integer.
                  *
                  * @param val Value.
                  */
-                void WriteInt64(const int64_t val);
+                void WriteInt64(int64_t val);
 
                 /**
                  * Write signed 64-byte integer.
@@ -149,7 +156,7 @@
                  * @param pos Position.
                  * @param val Value.
                  */
-                void WriteInt64(const int32_t pos, const int64_t val);
+                void WriteInt64(int32_t pos, int64_t val);
 
                 /**
                  * Write signed 64-byte integer array.
@@ -157,14 +164,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteInt64Array(const int64_t* val, const int32_t len);
+                void WriteInt64Array(const int64_t* val, int32_t len);
 
                 /**
                  * Write float.
                  *
                  * @param val Value.
                  */
-                void WriteFloat(const float val);
+                void WriteFloat(float val);
 
                 /**
                  * Write float array.
@@ -172,14 +179,14 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteFloatArray(const float* val, const int32_t len);
+                void WriteFloatArray(const float* val, int32_t len);
 
                 /**
                  * Write double.
                  *
                  * @param val Value.
                  */
-                void WriteDouble(const double val);
+                void WriteDouble(double val);
 
                 /**
                  * Write double array.
@@ -187,7 +194,7 @@
                  * @param val Value.
                  * @param len Length.
                  */
-                void WriteDoubleArray(const double* val, const int32_t len);
+                void WriteDoubleArray(const double* val, int32_t len);
 
                 /**
                  * Get current stream position.
@@ -199,7 +206,7 @@
                  *
                  * @param val Position (absolute).
                  */
-                void Position(const int32_t val);
+                void Position(int32_t val);
 
                 /**
                  * Reserve specified number of bytes in stream.
@@ -257,7 +264,7 @@
                  * @param off Offset.
                  * @param len Length.
                  */
-                void CopyAndShift(const int8_t* src, int32_t off, int32_t len);
+                void CopyAndShift(const void* src, int32_t off, int32_t len);
             };
         }
     }
diff --git a/modules/platforms/cpp/binary/src/binary/binary_type.cpp b/modules/platforms/cpp/binary/src/binary/binary_type.cpp
index 1bbf73c..f09e7dc 100644
--- a/modules/platforms/cpp/binary/src/binary/binary_type.cpp
+++ b/modules/platforms/cpp/binary/src/binary/binary_type.cpp
@@ -21,6 +21,7 @@
 {
     namespace binary
     {
+        IGNORE_SIGNED_OVERFLOW
         int32_t GetBinaryStringHashCode(const char* val)
         {
             if (val)
diff --git a/modules/platforms/cpp/binary/src/impl/binary/binary_schema.cpp b/modules/platforms/cpp/binary/src/impl/binary/binary_schema.cpp
index f7d6b9d..a3f5cfa 100644
--- a/modules/platforms/cpp/binary/src/impl/binary/binary_schema.cpp
+++ b/modules/platforms/cpp/binary/src/impl/binary/binary_schema.cpp
@@ -42,6 +42,7 @@
                 delete fieldsInfo;
             }
 
+            IGNORE_SIGNED_OVERFLOW
             void BinarySchema::AddField(int32_t fieldId, int32_t offset)
             {
                 if (!id)
diff --git a/modules/platforms/cpp/binary/src/impl/binary/binary_utils.cpp b/modules/platforms/cpp/binary/src/impl/binary/binary_utils.cpp
index 0b431ac..95b7d79 100644
--- a/modules/platforms/cpp/binary/src/impl/binary/binary_utils.cpp
+++ b/modules/platforms/cpp/binary/src/impl/binary/binary_utils.cpp
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-#include <time.h>
+#include <cstring>
 
 #include "ignite/ignite_error.h"
 
@@ -36,7 +36,7 @@
      * @param pos Position.
      * @param len Data to read.
      */
-    inline void CheckEnoughData(InteropMemory& mem, int32_t pos, int32_t len)
+    inline void CheckEnoughData(const InteropMemory& mem, int32_t pos, int32_t len)
     {
         if (mem.Length() < (pos + len))
         {
@@ -55,11 +55,13 @@
      * @return Primitive.
      */
     template<typename T>
-    inline T ReadPrimitive(InteropMemory& mem, int32_t pos)
+    inline T ReadPrimitive(const InteropMemory& mem, int32_t pos)
     {
         CheckEnoughData(mem, pos, sizeof(T));
 
-        return *reinterpret_cast<T*>(mem.Data() + pos);
+        T res;
+        std::memcpy(&res, mem.Data() + pos, sizeof(res));
+        return res;
     }
 
     /**
@@ -71,9 +73,11 @@
      * @return Primitive.
      */
     template<typename T>
-    inline T UnsafeReadPrimitive(InteropMemory& mem, int32_t pos)
+    inline T UnsafeReadPrimitive(const InteropMemory& mem, int32_t pos)
     {
-        return *reinterpret_cast<T*>(mem.Data() + pos);
+        T res;
+        std::memcpy(&res, mem.Data() + pos, sizeof(res));
+        return res;
     }
 }
 
@@ -83,6 +87,7 @@
     {
         namespace binary
         {
+            IGNORE_SIGNED_OVERFLOW
             int32_t BinaryUtils::GetDataHashCode(const void * data, size_t size)
             {
                 if (data)
@@ -204,12 +209,12 @@
                 return stream->ReadInt32();
             }
 
-            int32_t BinaryUtils::ReadInt32(InteropMemory& mem, int32_t pos)
+            int32_t BinaryUtils::ReadInt32(const InteropMemory& mem, int32_t pos)
             {
                 return ReadPrimitive<int32_t>(mem, pos);
             }
 
-            int32_t BinaryUtils::UnsafeReadInt32(InteropMemory& mem, int32_t pos)
+            int32_t BinaryUtils::UnsafeReadInt32(const InteropMemory& mem, int32_t pos)
             {
                 return UnsafeReadPrimitive<int32_t>(mem, pos);
             }
diff --git a/modules/platforms/cpp/binary/src/impl/interop/interop_input_stream.cpp b/modules/platforms/cpp/binary/src/impl/interop/interop_input_stream.cpp
index 907c840..eef4011 100644
--- a/modules/platforms/cpp/binary/src/impl/interop/interop_input_stream.cpp
+++ b/modules/platforms/cpp/binary/src/impl/interop/interop_input_stream.cpp
@@ -19,58 +19,90 @@
 
 #include <ignite/ignite_error.h>
 
-#include "ignite/impl/interop//interop_input_stream.h"
-
-/**
- * Common macro to read a single value.
- */
-#define IGNITE_INTEROP_IN_READ(type, len) { \
-    EnsureEnoughData(len); \
-    type res = *reinterpret_cast<type*>(data + pos); \
-    Shift(len); \
-    return res; \
-}
-
-/**
- * Common macro to read an array.
- */
-#define IGNITE_INTEROP_IN_READ_ARRAY(len, shift) { \
-    CopyAndShift(reinterpret_cast<int8_t*>(res), 0, ((len) << (shift))); \
-}
+#include "ignite/impl/interop/interop_input_stream.h"
 
 namespace ignite
 {
     namespace impl
     {
-        namespace interop 
+        namespace interop
         {
-            InteropInputStream::InteropInputStream(InteropMemory* mem)
+            class InputStreamHelper
             {
-                this->mem = mem;
+            public:
+                explicit InputStreamHelper(InteropInputStream& is0) : is(is0)
+                {
+                    // No-op;
+                };
 
-                data = mem->Data();
-                len = mem->Length();
-                pos = 0;
+                template<typename T>
+                T ReadPrimitiveAt(int32_t pos)
+                {
+                    int delta = pos + sizeof(T) - is.pos;
+
+                    if (delta > 0)
+                        is.EnsureEnoughData(delta);
+
+                    T res;
+                    std::memcpy(&res, is.data + pos, sizeof(T));
+                    return res;
+                }
+
+                template<typename T>
+                T ReadPrimitive()
+                {
+                    is.EnsureEnoughData(sizeof(T));
+
+                    T res;
+                    std::memcpy(&res, is.data + is.pos, sizeof(T));
+                    is.Shift(sizeof(T));
+                    return res;
+                }
+
+                template<typename T>
+                void ReadPrimitiveArray(T* res, int32_t len)
+                {
+                    is.CopyAndShift(res, 0, len * sizeof(T));
+                }
+
+            private:
+                InteropInputStream& is;
+            };
+
+            InteropInputStream::InteropInputStream(const InteropMemory* mem) :
+                mem(mem),
+                data(mem->Data()),
+                len(mem->Length()),
+                pos(0)
+            {
+                // No-op.
+            }
+
+            InteropInputStream::InteropInputStream(const InteropMemory *mem, int32_t len) :
+                mem(mem),
+                data(mem->Data()),
+                len(len),
+                pos(0)
+            {
+                if (len > mem->Length())
+                    IGNITE_ERROR_FORMATTED_3(IgniteError::IGNITE_ERR_MEMORY,
+                        "Requested input stream len is greater than memories length",
+                             "memPtr", mem->PointerLong(), "len", len, "memLen", mem->Length());
             }
 
             int8_t InteropInputStream::ReadInt8()
             {
-                IGNITE_INTEROP_IN_READ(int8_t, 1);
+                return InputStreamHelper(*this).ReadPrimitive<int8_t>();
             }
 
-            int32_t InteropInputStream::ReadInt8(int32_t pos)
+            int8_t InteropInputStream::ReadInt8(int32_t pos)
             {
-                int delta = pos + 1 - this->pos;
-
-                if (delta > 0)
-                    EnsureEnoughData(delta);
-
-                return *reinterpret_cast<int8_t*>(data + pos);
+                return InputStreamHelper(*this).ReadPrimitiveAt<int8_t>(pos);
             }
 
-            void InteropInputStream::ReadInt8Array(int8_t* const res, const int32_t len)
+            void InteropInputStream::ReadInt8Array(int8_t* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 0);
+                return InputStreamHelper(*this).ReadPrimitiveArray<int8_t>(res, len);
             }
 
             bool InteropInputStream::ReadBool()
@@ -78,7 +110,7 @@
                 return ReadInt8() == 1;
             }
 
-            void InteropInputStream::ReadBoolArray(bool* const res, const int32_t len)
+            void InteropInputStream::ReadBoolArray(bool* res, int32_t len)
             {
                 for (int i = 0; i < len; i++)
                     *(res + i) = ReadBool();
@@ -86,62 +118,52 @@
 
             int16_t InteropInputStream::ReadInt16()
             {
-                IGNITE_INTEROP_IN_READ(int16_t, 2);
+                return InputStreamHelper(*this).ReadPrimitive<int16_t>();
             }
 
-            int32_t InteropInputStream::ReadInt16(int32_t pos)
+            int16_t InteropInputStream::ReadInt16(int32_t pos)
             {
-                int delta = pos + 2 - this->pos;
-
-                if (delta > 0)
-                    EnsureEnoughData(delta);
-
-                return *reinterpret_cast<int16_t*>(data + pos);
+                return InputStreamHelper(*this).ReadPrimitiveAt<int16_t>(pos);
             }
 
-            void InteropInputStream::ReadInt16Array(int16_t* const res, const int32_t len)
+            void InteropInputStream::ReadInt16Array(int16_t* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 1);
+                return InputStreamHelper(*this).ReadPrimitiveArray<int16_t>(res, len);
             }
 
             uint16_t InteropInputStream::ReadUInt16()
             {
-                IGNITE_INTEROP_IN_READ(uint16_t, 2);
+                return InputStreamHelper(*this).ReadPrimitive<uint16_t>();
             }
 
-            void InteropInputStream::ReadUInt16Array(uint16_t* const res, const int32_t len)
+            void InteropInputStream::ReadUInt16Array(uint16_t* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 1);
+                return InputStreamHelper(*this).ReadPrimitiveArray<uint16_t>(res, len);
             }
 
             int32_t InteropInputStream::ReadInt32()
             {
-                IGNITE_INTEROP_IN_READ(int32_t, 4);
+                return InputStreamHelper(*this).ReadPrimitive<int32_t>();
             }
 
             int32_t InteropInputStream::ReadInt32(int32_t pos)
             {
-                int delta = pos + 4 - this->pos;
-
-                if (delta > 0)
-                    EnsureEnoughData(delta);
-
-                return *reinterpret_cast<int32_t*>(data + pos);
+                return InputStreamHelper(*this).ReadPrimitiveAt<int32_t>(pos);
             }
 
-            void InteropInputStream::ReadInt32Array(int32_t* const res, const int32_t len)
+            void InteropInputStream::ReadInt32Array(int32_t* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 2);
+                return InputStreamHelper(*this).ReadPrimitiveArray<int32_t>(res, len);
             }
 
             int64_t InteropInputStream::ReadInt64()
             {
-                IGNITE_INTEROP_IN_READ(int64_t, 8);
+                return InputStreamHelper(*this).ReadPrimitive<int64_t>();
             }
 
-            void InteropInputStream::ReadInt64Array(int64_t* const res, const int32_t len)
+            void InteropInputStream::ReadInt64Array(int64_t* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 3);
+                return InputStreamHelper(*this).ReadPrimitiveArray<int64_t>(res, len);
             }
 
             float InteropInputStream::ReadFloat()
@@ -153,9 +175,9 @@
                 return u.f;
             }
 
-            void InteropInputStream::ReadFloatArray(float* const res, const int32_t len)
+            void InteropInputStream::ReadFloatArray(float* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 2);
+                return InputStreamHelper(*this).ReadPrimitiveArray<float>(res, len);
             }
 
             double InteropInputStream::ReadDouble()
@@ -167,9 +189,9 @@
                 return u.d;
             }
 
-            void InteropInputStream::ReadDoubleArray(double* const res, const int32_t len)
+            void InteropInputStream::ReadDoubleArray(double* res, int32_t len)
             {
-                IGNITE_INTEROP_IN_READ_ARRAY(len, 3);
+                return InputStreamHelper(*this).ReadPrimitiveArray<double>(res, len);
             }
 
             int32_t InteropInputStream::Remaining() const
@@ -213,11 +235,12 @@
                 }
             }
 
-            void InteropInputStream::CopyAndShift(int8_t* dest, int32_t off, int32_t cnt)
+            void InteropInputStream::CopyAndShift(void* dest, int32_t off, int32_t cnt)
             {
                 EnsureEnoughData(cnt);
 
-                memcpy(dest + off, data + pos, cnt);
+                if (dest != 0)
+                    std::memcpy(static_cast<int8_t*>(dest) + off, data + pos, cnt);
 
                 Shift(cnt);
             }
diff --git a/modules/platforms/cpp/binary/src/impl/interop/interop_memory.cpp b/modules/platforms/cpp/binary/src/impl/interop/interop_memory.cpp
index 443bd25..8efd959 100644
--- a/modules/platforms/cpp/binary/src/impl/interop/interop_memory.cpp
+++ b/modules/platforms/cpp/binary/src/impl/interop/interop_memory.cpp
@@ -100,7 +100,7 @@
                 return memPtr;
             }
 
-            int64_t InteropMemory::PointerLong()
+            int64_t InteropMemory::PointerLong() const
             {
                 return reinterpret_cast<int64_t>(memPtr);
             }
diff --git a/modules/platforms/cpp/binary/src/impl/interop/interop_output_stream.cpp b/modules/platforms/cpp/binary/src/impl/interop/interop_output_stream.cpp
index bbf28f6..c36de46 100644
--- a/modules/platforms/cpp/binary/src/impl/interop/interop_output_stream.cpp
+++ b/modules/platforms/cpp/binary/src/impl/interop/interop_output_stream.cpp
@@ -21,28 +21,45 @@
 
 #include "ignite/impl/interop//interop_output_stream.h"
 
-/**
- * Common macro to write a single value.
- */
-#define IGNITE_INTEROP_OUT_WRITE(val, type, len) { \
-    EnsureCapacity(pos + len); \
-    *reinterpret_cast<type*>(data + pos) = val; \
-    Shift(len); \
-}
-
-/**
- * Common macro to write an array.
- */
-#define IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len) { \
-    CopyAndShift(reinterpret_cast<const int8_t*>(val), 0, len); \
-}
-
 namespace ignite
 {
     namespace impl
     {
-        namespace interop 
+        namespace interop
         {
+            class OutputStreamHelper
+            {
+            public:
+                explicit OutputStreamHelper(InteropOutputStream& os0) : os(os0)
+                {
+                    // No-op;
+                };
+
+                template<typename T>
+                void WritePrimitiveAt(const T& val, int32_t pos)
+                {
+                    os.EnsureCapacity(pos + sizeof(T));
+                    std::memcpy(os.data + pos, &val, sizeof(T));
+                }
+
+                template<typename T>
+                void WritePrimitive(const T& val)
+                {
+                    os.EnsureCapacity(os.pos + sizeof(T));
+                    std::memcpy(os.data + os.pos, &val, sizeof(T));
+                    os.Shift(sizeof(T));
+                }
+
+                template<typename T>
+                void WritePrimitiveArray(const T* arr, int32_t len)
+                {
+                    os.CopyAndShift(arr, 0, len * sizeof(T));
+                }
+
+            private:
+                InteropOutputStream& os;
+            };
+
             InteropOutputStream::InteropOutputStream(InteropMemory* mem)
             {
                 this->mem = mem;
@@ -52,29 +69,27 @@
                 pos = 0;
             }
 
-            void InteropOutputStream::WriteInt8(const int8_t val)
+            void InteropOutputStream::WriteInt8(int8_t val)
             {
-                IGNITE_INTEROP_OUT_WRITE(val, int8_t, 1);
+                OutputStreamHelper(*this).WritePrimitive<int8_t>(val);
             }
 
-            void InteropOutputStream::WriteInt8(const int8_t val, const int32_t pos)
+            void InteropOutputStream::WriteInt8(int8_t val, int32_t pos0)
             {
-                EnsureCapacity(pos + 1);
-
-                *(data + pos) = val;
+                OutputStreamHelper(*this).WritePrimitiveAt<int8_t>(val, pos0);
             }
 
-            void InteropOutputStream::WriteInt8Array(const int8_t* val, const int32_t len)
+            void InteropOutputStream::WriteInt8Array(const int8_t* val, int32_t len)
             {
-                IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len);
+                OutputStreamHelper(*this).WritePrimitiveArray<int8_t>(val, len);
             }
 
-            void InteropOutputStream::WriteBool(const bool val)
+            void InteropOutputStream::WriteBool(bool val)
             {
                 WriteInt8(val ? 1 : 0);
             }
 
-            void InteropOutputStream::WriteBoolArray(const bool* val, const int32_t len)
+            void InteropOutputStream::WriteBoolArray(const bool* val, int32_t len)
             {
                 for (int i = 0; i < len; i++)
                     WriteBool(*(val + i));
@@ -82,90 +97,80 @@
 
             void InteropOutputStream::WriteInt16(const int16_t val)
             {
-                IGNITE_INTEROP_OUT_WRITE(val, int16_t, 2);
+                OutputStreamHelper(*this).WritePrimitive<int16_t>(val);
             }
 
-            void InteropOutputStream::WriteInt16(const int32_t pos, const int16_t val)
+            void InteropOutputStream::WriteInt16(int32_t pos0, int16_t val)
             {
-                EnsureCapacity(pos + 2);
-
-                *reinterpret_cast<int16_t*>(data + pos) = val;
+                OutputStreamHelper(*this).WritePrimitiveAt<int16_t>(val, pos0);
             }
 
-            void InteropOutputStream::WriteInt16Array(const int16_t* val, const int32_t len)
+            void InteropOutputStream::WriteInt16Array(const int16_t* val, int32_t len)
             {
-                IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len << 1);
+                OutputStreamHelper(*this).WritePrimitiveArray<int16_t>(val, len);
             }
 
-            void InteropOutputStream::WriteUInt16(const uint16_t val)
+            void InteropOutputStream::WriteUInt16(uint16_t val)
             {
-                IGNITE_INTEROP_OUT_WRITE(val, uint16_t, 2);
+                OutputStreamHelper(*this).WritePrimitive<uint16_t>(val);
             }
 
-            void InteropOutputStream::WriteUInt16Array(const uint16_t* val, const int32_t len)
+            void InteropOutputStream::WriteUInt16Array(const uint16_t* val, int32_t len)
             {
-                IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len << 1);
+                OutputStreamHelper(*this).WritePrimitiveArray<uint16_t>(val, len);
             }
 
-            void InteropOutputStream::WriteInt32(const int32_t val)
+            void InteropOutputStream::WriteInt32(int32_t val)
             {
-                IGNITE_INTEROP_OUT_WRITE(val, int32_t, 4);
+                OutputStreamHelper(*this).WritePrimitive<int32_t>(val);
             }
 
-            void InteropOutputStream::WriteInt32(const int32_t pos, const int32_t val)
+            void InteropOutputStream::WriteInt32(int32_t pos0, int32_t val)
             {
-                EnsureCapacity(pos + 4);
-
-                *reinterpret_cast<int32_t*>(data + pos) = val;
+                OutputStreamHelper(*this).WritePrimitiveAt<int32_t>(val, pos0);
             }
 
-            void InteropOutputStream::WriteInt32Array(const int32_t* val, const int32_t len)
+            void InteropOutputStream::WriteInt32Array(const int32_t* val, int32_t len)
             {
-                IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len << 2);
+                OutputStreamHelper(*this).WritePrimitiveArray<int32_t>(val, len);
             }
 
-            void InteropOutputStream::WriteInt64(const int64_t val)
+            void InteropOutputStream::WriteInt64(int64_t val)
             {
-                IGNITE_INTEROP_OUT_WRITE(val, int64_t, 8);
+                OutputStreamHelper(*this).WritePrimitive<int64_t>(val);
             }
 
-            void InteropOutputStream::WriteInt64(const int32_t pos, const int64_t val)
+            void InteropOutputStream::WriteInt64(const int32_t pos0, const int64_t val)
             {
-                EnsureCapacity(pos + 8);
-
-                *reinterpret_cast<int64_t*>(data + pos) = val;
+                OutputStreamHelper(*this).WritePrimitiveAt<int64_t>(val, pos0);
             }
 
-            void InteropOutputStream::WriteInt64Array(const int64_t* val, const int32_t len)
+            void InteropOutputStream::WriteInt64Array(const int64_t* val, int32_t len)
             {
-                IGNITE_INTEROP_OUT_WRITE_ARRAY(val, len << 3);
+                OutputStreamHelper(*this).WritePrimitiveArray<int64_t>(val, len);
             }
 
-            void InteropOutputStream::WriteFloat(const float val)
+            void InteropOutputStream::WriteFloat(float val)
             {
-                BinaryFloatInt32 u;
-
-                u.f = val;
+                BinaryFloatInt32 u = {val};
 
                 WriteInt32(u.i);
             }
 
-            void InteropOutputStream::WriteFloatArray(const float* val, const int32_t len)
+            void InteropOutputStream::WriteFloatArray(const float* val, int32_t len)
             {
                 for (int i = 0; i < len; i++)
                     WriteFloat(*(val + i));
             }
 
-            void InteropOutputStream::WriteDouble(const double val)
+            void InteropOutputStream::WriteDouble(double val)
             {
-                BinaryDoubleInt64 u;
-
-                u.d = val;
+                BinaryDoubleInt64 u = {val};
 
                 WriteInt64(u.i);
             }
 
-            void InteropOutputStream::WriteDoubleArray(const double* val, const int32_t len)
+            void InteropOutputStream::WriteDoubleArray(const double* val, int32_t len)
             {
                 for (int i = 0; i < len; i++)
                     WriteDouble(*(val + i));
@@ -176,7 +181,7 @@
                 return pos;
             }
 
-            void InteropOutputStream::Position(const int32_t val)
+            void InteropOutputStream::Position(int32_t val)
             {
                 EnsureCapacity(val);
 
@@ -221,14 +226,13 @@
                 pos += cnt;
             }
 
-            void InteropOutputStream::CopyAndShift(const int8_t* src, int32_t off, int32_t len) {
+            void InteropOutputStream::CopyAndShift(const void* src, int32_t off, int32_t len) {
                 EnsureCapacity(pos + len);
 
-                memcpy(data + pos, src + off, len);
+                memcpy(data + pos, reinterpret_cast<const int8_t*>(src) + off, len);
 
                 Shift(len);
             }
         }
     }
 }
-
diff --git a/modules/platforms/cpp/common/CMakeLists.txt b/modules/platforms/cpp/common/CMakeLists.txt
index 5469dd6..6e8cad6 100644
--- a/modules/platforms/cpp/common/CMakeLists.txt
+++ b/modules/platforms/cpp/common/CMakeLists.txt
@@ -25,6 +25,7 @@
         src/common/bits.cpp
         src/common/concurrent.cpp
         src/common/decimal.cpp
+        src/common/thread_pool.cpp
         src/common/utils.cpp
         src/date.cpp
         src/ignite_error.cpp
diff --git a/modules/platforms/cpp/common/include/ignite/common/bits.h b/modules/platforms/cpp/common/include/ignite/common/bits.h
index 74a5f5e..b836655 100644
--- a/modules/platforms/cpp/common/include/ignite/common/bits.h
+++ b/modules/platforms/cpp/common/include/ignite/common/bits.h
@@ -44,6 +44,16 @@
             IGNITE_IMPORT_EXPORT int32_t NumberOfTrailingZerosI32(int32_t i);
 
             /**
+             * Get number of trailing zero bits in the binary representation of the specified
+             * 32-bit unsigned int value.
+             *
+             * @param i The value whose bits are to be counted.
+             * @return The number of trailing zero bits in the binary representation
+             *     of the specified 32-bit unsigned int value.
+             */
+            IGNITE_IMPORT_EXPORT int32_t NumberOfTrailingZerosU32(uint32_t i);
+
+            /**
              * Get number of leading zero bits in the two's complement binary
              * representation of the specified 32-bit int value.
              *
@@ -54,15 +64,15 @@
             IGNITE_IMPORT_EXPORT int32_t NumberOfLeadingZerosI32(int32_t i);
 
             /**
-             * Get number of leading zero bits in the two's complement binary
-             * representation of the specified 32-bit int value.
+             * Get number of leading zero bits in the binary representation
+             * of the specified unsigned 32-bit int value.
              *
              * @param i The value whose bits are to be counted.
-             * @return The number of leading zero bits in the two's complement
-             *     binary representation of the specified 32-bit int value.
+             * @return The number of leading zero bits in the binary representation
+             *     of the specified unsigned 32-bit int value.
              */
             IGNITE_IMPORT_EXPORT int32_t NumberOfLeadingZerosU32(uint32_t i);
-            
+
             /**
              * Get number of leading zero bits in the two's complement binary
              * representation of the specified 64-bit int value.
@@ -72,7 +82,7 @@
              *     binary representation of the specified 64-bit int value.
              */
             IGNITE_IMPORT_EXPORT int32_t NumberOfLeadingZerosI64(int64_t i);
-            
+
             /**
              * Get number of leading zero bits in the two's complement binary
              * representation of the specified 64-bit int value.
diff --git a/modules/platforms/cpp/common/include/ignite/common/concurrent.h b/modules/platforms/cpp/common/include/ignite/common/concurrent.h
index 290efd7..490e005 100644
--- a/modules/platforms/cpp/common/include/ignite/common/concurrent.h
+++ b/modules/platforms/cpp/common/include/ignite/common/concurrent.h
@@ -262,6 +262,7 @@
                 /**
                  * Destructor.
                  */
+                IGNORE_FALSE_UNDEFINED
                 ~SharedPointer()
                 {
                     if (impl && impl->Decrement())
@@ -293,7 +294,7 @@
                  *
                  * @return Raw pointer.
                  */
-                const T* Get() const
+                T* Get() const
                 {
                     return ptr;
                 }
diff --git a/modules/platforms/cpp/common/include/ignite/common/factory.h b/modules/platforms/cpp/common/include/ignite/common/factory.h
new file mode 100644
index 0000000..b85f58b
--- /dev/null
+++ b/modules/platforms/cpp/common/include/ignite/common/factory.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_COMMON_FACTORY
+#define _IGNITE_COMMON_FACTORY
+
+#include <ignite/common/concurrent.h>
+
+namespace ignite
+{
+    namespace common
+    {
+        /**
+         * Factory class.
+         *
+         * @tparam T Instances of this type factory builds.
+         */
+        template<typename T>
+        class Factory
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~Factory()
+            {
+                // No-op.
+            }
+
+            /**
+             * Build instance.
+             *
+             * @return New instance of type @c T.
+             */
+            virtual common::concurrent::SharedPointer<T> Build() = 0;
+        };
+    }
+}
+
+#endif //_IGNITE_COMMON_FACTORY
\ No newline at end of file
diff --git a/modules/platforms/cpp/common/include/ignite/common/platform_utils.h b/modules/platforms/cpp/common/include/ignite/common/platform_utils.h
index 59d4d8b..260f5c5 100644
--- a/modules/platforms/cpp/common/include/ignite/common/platform_utils.h
+++ b/modules/platforms/cpp/common/include/ignite/common/platform_utils.h
@@ -117,6 +117,13 @@
          * @return Random seed.
          */
         IGNITE_IMPORT_EXPORT unsigned GetRandSeed();
+
+        /**
+         * Try extract from system error stack, and return platform-specific error.
+         *
+         * @return Error in human-readable format.
+         */
+        IGNITE_IMPORT_EXPORT std::string GetLastSystemError();
     }
 }
 
diff --git a/modules/platforms/cpp/common/include/ignite/common/promise.h b/modules/platforms/cpp/common/include/ignite/common/promise.h
index 8fe9603..59f3ecf 100644
--- a/modules/platforms/cpp/common/include/ignite/common/promise.h
+++ b/modules/platforms/cpp/common/include/ignite/common/promise.h
@@ -213,6 +213,98 @@
             /** Shared state. */
             concurrent::SharedPointer< SharedState<ValueType> > state;
         };
+
+        /**
+         * Specialization for SharePointer type.
+         */
+        template<typename T>
+        class Promise< concurrent::SharedPointer<T> >
+        {
+        public:
+            /** Template value type */
+            typedef T ValueType;
+
+            /** Template value type wrapped in shared pointer */
+            typedef concurrent::SharedPointer<ValueType> SP_ValueType;
+
+            /**
+             * Constructor.
+             */
+            Promise() :
+                state(new SharedState<SP_ValueType>())
+            {
+                // No-op.
+            }
+
+            /**
+             * Destructor.
+             */
+            ~Promise()
+            {
+                SharedState<SP_ValueType>* state0 = state.Get();
+
+                assert(state0 != 0);
+
+                if (!state0->IsSet())
+                    state0->SetError(IgniteError(IgniteError::IGNITE_ERR_FUTURE_STATE,
+                        "Broken promise. Value will never be set due to internal error."));
+            }
+
+
+            /**
+             * Get future for this promise.
+             *
+             * @return New future instance.
+             */
+            Future<SP_ValueType> GetFuture() const
+            {
+                return Future<SP_ValueType>(state);
+            }
+
+            /**
+             * Set value.
+             *
+             * @throw IgniteError with IgniteError::IGNITE_ERR_FUTURE_STATE if error or value has been set already.
+             * @param val Value to set.
+             */
+            void SetValue(SP_ValueType val)
+            {
+                SharedState<SP_ValueType>* state0 = state.Get();
+
+                assert(state0 != 0);
+
+                return state0->SetValue(val);
+            }
+
+            /**
+             * Set error.
+             *
+             * @throw IgniteError with IgniteError::IGNITE_ERR_FUTURE_STATE if error or value has been set already.
+             * @param err Error to set.
+             */
+            void SetError(const IgniteError& err)
+            {
+                SharedState<SP_ValueType>* state0 = state.Get();
+
+                assert(state0 != 0);
+
+                state0->SetError(err);
+            }
+
+            /**
+             * Set cancel target.
+             */
+            void SetCancelTarget(std::auto_ptr<Cancelable>& target)
+            {
+                state.Get()->SetCancelTarget(target);
+            }
+
+        private:
+            IGNITE_NO_COPY_ASSIGNMENT(Promise);
+
+            /** Shared state. */
+            concurrent::SharedPointer< SharedState<SP_ValueType> > state;
+        };
     }
 }
 
diff --git a/modules/platforms/cpp/common/include/ignite/common/shared_state.h b/modules/platforms/cpp/common/include/ignite/common/shared_state.h
index d223753..e6fb9fe 100644
--- a/modules/platforms/cpp/common/include/ignite/common/shared_state.h
+++ b/modules/platforms/cpp/common/include/ignite/common/shared_state.h
@@ -376,6 +376,180 @@
             /** Lock that used to prevent double-set of the value. */
             mutable concurrent::CriticalSection mutex;
         };
+
+        /**
+         * Specialization for shared pointer type.
+         */
+        template<typename T>
+        class SharedState< concurrent::SharedPointer<T> >
+        {
+        public:
+            /** Template value type */
+            typedef T ValueType;
+
+            /**
+             * Default constructor.
+             * Constructs non-set SharedState instance.
+             */
+            SharedState() :
+                value(),
+                error()
+            {
+                // No-op.
+            }
+
+            /**
+             * Destructor.
+             */
+            ~SharedState()
+            {
+                // No-op.
+            }
+
+            /**
+             * Checks if the value or error set for the state.
+             * @return True if the value or error set for the state.
+             */
+            bool IsSet() const
+            {
+                return value.IsValid() || error.GetCode() != IgniteError::IGNITE_SUCCESS;
+            }
+
+            /**
+             * Set value.
+             *
+             * @throw IgniteError with IgniteError::IGNITE_ERR_FUTURE_STATE if error or value has been set already.
+             * @param val Value to set.
+             */
+            void SetValue(const concurrent::SharedPointer<ValueType>& val)
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                if (IsSet())
+                {
+                    if (value.IsValid())
+                        throw IgniteError(IgniteError::IGNITE_ERR_FUTURE_STATE, "Future value already set");
+
+                    if (error.GetCode() != IgniteError::IGNITE_SUCCESS)
+                        throw IgniteError(IgniteError::IGNITE_ERR_FUTURE_STATE, "Future error already set");
+                }
+
+                value = val;
+
+                cond.NotifyAll();
+            }
+
+            /**
+             * Set error.
+             *
+             * @throw IgniteError with IgniteError::IGNITE_ERR_FUTURE_STATE if error or value has been set already.
+             * @param err Error to set.
+             */
+            void SetError(const IgniteError& err)
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                if (IsSet())
+                {
+                    if (value.IsValid())
+                        throw IgniteError(IgniteError::IGNITE_ERR_FUTURE_STATE, "Future value already set");
+
+                    if (error.GetCode() != IgniteError::IGNITE_SUCCESS)
+                        throw IgniteError(IgniteError::IGNITE_ERR_FUTURE_STATE, "Future error already set");
+                }
+
+                error = err;
+
+                cond.NotifyAll();
+            }
+
+            /**
+             * Wait for value to be set.
+             * Active thread will be blocked until value or error will be set.
+             */
+            void Wait() const
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                while (!IsSet())
+                    cond.Wait(mutex);
+            }
+
+            /**
+             * Wait for value to be set for specified time.
+             * Active thread will be blocked until value or error will be set or timeout will end.
+             *
+             * @param msTimeout Timeout in milliseconds.
+             * @return True if the object has been triggered and false in case of timeout.
+             */
+            bool WaitFor(int32_t msTimeout) const
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                if (IsSet())
+                    return true;
+
+                return cond.WaitFor(mutex, msTimeout);
+            }
+
+            /**
+             * Get the set value.
+             * Active thread will be blocked until value or error will be set.
+             *
+             * @throw IgniteError if error has been set.
+             * @return Value that has been set on success.
+             */
+            concurrent::SharedPointer<ValueType> GetValue() const
+            {
+                Wait();
+
+                if (value.IsValid())
+                    return value;
+
+                assert(error.GetCode() != IgniteError::IGNITE_SUCCESS);
+
+                throw error;
+            }
+
+            /**
+             * Set cancel target.
+             */
+            void SetCancelTarget(std::auto_ptr<Cancelable>& target)
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                cancelTarget = target;
+            }
+
+            /**
+             * Cancel related operation.
+             */
+            void Cancel()
+            {
+                concurrent::CsLockGuard guard(mutex);
+
+                if (cancelTarget.get())
+                    cancelTarget->Cancel();
+            }
+
+        private:
+            IGNITE_NO_COPY_ASSIGNMENT(SharedState);
+
+            /** Cancel target. */
+            std::auto_ptr<Cancelable> cancelTarget;
+
+            /** Value. */
+            concurrent::SharedPointer<ValueType> value;
+
+            /** Error. */
+            IgniteError error;
+
+            /** Condition variable which serves to signal that value is set. */
+            mutable concurrent::ConditionVariable cond;
+
+            /** Lock that used to prevent double-set of the value. */
+            mutable concurrent::CriticalSection mutex;
+        };
     }
 }
 
diff --git a/modules/platforms/cpp/common/include/ignite/common/thread_pool.h b/modules/platforms/cpp/common/include/ignite/common/thread_pool.h
new file mode 100644
index 0000000..ef05213
--- /dev/null
+++ b/modules/platforms/cpp/common/include/ignite/common/thread_pool.h
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+/**
+ * @file
+ * Declares ignite::common::ThreadPool class.
+ */
+
+#ifndef _IGNITE_COMMON_THREAD_POOL
+#define _IGNITE_COMMON_THREAD_POOL
+
+#include <deque>
+#include <vector>
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/common.h>
+#include <ignite/common/concurrent.h>
+
+namespace ignite
+{
+    namespace common
+    {
+        /**
+         * Thread pool task.
+         */
+        class ThreadPoolTask
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~ThreadPoolTask()
+            {
+                // No-op.
+            }
+
+            /**
+             * Execute task.
+             */
+            virtual void Execute() = 0;
+
+            /**
+             * Called if error occurred during task processing.
+             *
+             * @param err Error.
+             */
+             virtual void OnError(const IgniteError& err) = 0;
+        };
+
+        /** Shared pointer to thread pool task. */
+        typedef concurrent::SharedPointer< ThreadPoolTask > SP_ThreadPoolTask;
+
+        /**
+         * Thread Pool.
+         */
+        class ThreadPool
+        {
+        public:
+            /**
+             * Constructor.
+             *
+             * @param threadsNum Number of threads. If set to 0 current number of processors is used.
+             */
+            IGNITE_IMPORT_EXPORT explicit ThreadPool(uint32_t threadsNum);
+
+            /**
+             * Destructor.
+             */
+            IGNITE_IMPORT_EXPORT virtual ~ThreadPool();
+
+            /**
+             * Start threads in pool.
+             */
+            IGNITE_IMPORT_EXPORT void Start();
+
+            /**
+             * Stop threads in pool.
+             *
+             * @warning Once stopped it can not be restarted.
+             */
+            IGNITE_IMPORT_EXPORT void Stop();
+
+            /**
+             * Dispatch task.
+             *
+             * @param task Task.
+             */
+            IGNITE_IMPORT_EXPORT void Dispatch(const SP_ThreadPoolTask& task);
+
+        private:
+            IGNITE_NO_COPY_ASSIGNMENT(ThreadPool);
+
+            /**
+             * Task queue.
+             */
+            class TaskQueue
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                TaskQueue();
+
+                /**
+                 * Destructor.
+                 */
+                ~TaskQueue();
+
+                /**
+                 * Push a new task to the queue.
+                 *
+                 * @param task Task. Should not be null.
+                 */
+                void Push(const SP_ThreadPoolTask& task);
+
+                /**
+                 * Pop a task from the queue.
+                 *
+                 * @return New task or null when unblocked.
+                 */
+                SP_ThreadPoolTask Pop();
+
+                /**
+                 * Unblock queue. When unblocked queue will not block or return new tasks.
+                 */
+                void Unblock();
+
+            private:
+                /** If true queue will not block. */
+                volatile bool unblocked;
+
+                /** Tasks queue. */
+                std::deque< SP_ThreadPoolTask > tasks;
+
+                /** Critical section. */
+                concurrent::CriticalSection mutex;
+
+                /** Condition variable. */
+                concurrent::ConditionVariable waitPoint;
+            };
+
+            /**
+             * Worker thread.
+             */
+            class WorkerThread : public concurrent::Thread
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param taskQueue Task queue.
+                 */
+                explicit WorkerThread(TaskQueue& taskQueue);
+
+                /**
+                 * Destructor.
+                 */
+                ~WorkerThread();
+
+            private:
+                /**
+                 * Run thread.
+                 */
+                virtual void Run();
+
+                /** Task queue. */
+                TaskQueue& taskQueue;
+            };
+
+            /**
+             * Handle an error that occurred during task execution.
+             *
+             * @param task Task.
+             * @param err Error.
+             */
+            static void HandleTaskError(ThreadPoolTask &task, const IgniteError &err);
+
+            /** Shared pointer to thread pool worker thread. */
+            typedef concurrent::SharedPointer< WorkerThread > SP_WorkerThread;
+
+            /** Started flag. */
+            bool started;
+
+            /** Stopped flag. */
+            bool stopped;
+
+            /** Task queue. */
+            TaskQueue queue;
+
+            /** Worker Threads. */
+            std::vector<SP_WorkerThread> threads;
+
+            /** Critical section. */
+            concurrent::CriticalSection mutex;
+        };
+    }
+}
+
+#endif //_IGNITE_COMMON_THREAD_POOL
diff --git a/modules/platforms/cpp/common/include/ignite/common/utils.h b/modules/platforms/cpp/common/include/ignite/common/utils.h
index 9d17d65..20fd9a7 100644
--- a/modules/platforms/cpp/common/include/ignite/common/utils.h
+++ b/modules/platforms/cpp/common/include/ignite/common/utils.h
@@ -594,7 +594,7 @@
         };
 
         /**
-         * Deinit guard class template.
+         * De-init guard class template.
          *
          * Upon destruction calls provided deinit function on provided instance.
          *
@@ -654,7 +654,107 @@
          * @param name Name without extension.
          * @return Full name.
          */
-        IGNITE_IMPORT_EXPORT std::string GetDynamicLibraryName(const char* name);
+        IGNITE_IMPORT_EXPORT std::string GetDynamicLibraryName(const std::string& name);
+
+        /**
+         * Get hex dump of binary data in string form.
+         * @param data Data.
+         * @param count Number of bytes.
+         * @return Hex dump string.
+         */
+        IGNITE_IMPORT_EXPORT std::string HexDump(const void* data, size_t count);
+
+        /**
+         * Fibonacci sequence iterator.
+         *
+         * @tparam S Sequence length. Should be >= 2.
+         */
+        template<size_t S>
+        class FibonacciSequence
+        {
+        public:
+            /** Size. */
+            static const size_t size = S > 2 ? S : 2;
+
+            /**
+             * Constructor.
+             */
+            FibonacciSequence()
+            {
+                sequence[0] = 0;
+                sequence[1] = 1;
+
+                for (size_t i = 2; i < size; ++i)
+                    sequence[i] = sequence[i - 1] + sequence[i - 2];
+            }
+
+            /**
+             * Get n-th or max member of sequence.
+             *
+             * @param n Member position.
+             * @return N-th member of sequence if n < size, or max member.
+             */
+            size_t GetValue(size_t n) const
+            {
+                if (n < size)
+                    return sequence[n];
+
+                return sequence[size-1];
+            }
+
+        private:
+            /** Sequence of fibonacci numbers */
+            size_t sequence[size];
+        };
+
+        /**
+         * Throw platform-specific error.
+         *
+         * @param msg Error message.
+         */
+        IGNITE_IMPORT_EXPORT void ThrowSystemError(const std::string& msg);
+
+        /**
+         * Try extract from system error stack and throw platform-specific error.
+         *
+         * @param description Error description.
+         * @param advice User advice.
+         */
+        IGNITE_IMPORT_EXPORT void ThrowLastSystemError(const std::string& description, const std::string& advice);
+
+        /**
+         * Try extract from system error stack and throw platform-specific error.
+         *
+         * @param description Error description.
+         */
+        IGNITE_IMPORT_EXPORT void ThrowLastSystemError(const std::string& description);
+
+        /**
+         * Format error message.
+         *
+         * @param description Error description.
+         * @param description Error details.
+         * @param advice User advice.
+         */
+        IGNITE_IMPORT_EXPORT std::string FormatErrorMessage(const std::string& description, const std::string& details,
+            const std::string& advice);
+
+        /**
+         * Try extract from system error stack, format and return platform-specific error.
+         *
+         * @param description Error description.
+         * @return Error in human-readable format.
+         */
+        IGNITE_IMPORT_EXPORT std::string GetLastSystemError(const std::string& description);
+
+        /**
+         * Try extract from system error stack, format and return platform-specific error.
+         *
+         * @param description Error description.
+         * @param advice User advice.
+         * @return Error in human-readable format.
+         */
+        IGNITE_IMPORT_EXPORT std::string GetLastSystemError(const std::string& description, const std::string& advice);
     }
 }
 
diff --git a/modules/platforms/cpp/common/include/ignite/future.h b/modules/platforms/cpp/common/include/ignite/future.h
index f709797..0b15363 100644
--- a/modules/platforms/cpp/common/include/ignite/future.h
+++ b/modules/platforms/cpp/common/include/ignite/future.h
@@ -279,6 +279,131 @@
         /** Shared state. */
         common::concurrent::SharedPointer< common::SharedState<ValueType> > state;
     };
+
+    /**
+     * Specialization for shared pointer.
+     */
+    template<typename T>
+    class Future< common::concurrent::SharedPointer<T> >
+    {
+        friend class common::Promise< common::concurrent::SharedPointer<T> >;
+
+    public:
+        /** Template value type */
+        typedef T ValueType;
+
+        /** Template value type shared pointer */
+        typedef common::concurrent::SharedPointer<ValueType> SP_ValueType;
+
+        /**
+         * Copy constructor.
+         *
+         * @param src Instance to copy.
+         */
+        Future(const Future<SP_ValueType>& src) :
+            state(src.state)
+        {
+            // No-op.
+        }
+
+        /**
+         * Assignment operator.
+         *
+         * @param other Other instance.
+         * @return *this.
+         */
+        Future& operator=(const Future<SP_ValueType>& other)
+        {
+            if (this != &other)
+                state = other.state;
+
+            return *this;
+        }
+
+        /**
+         * Wait for value to be set.
+         * Active thread will be blocked until value or error will be set.
+         */
+        void Wait() const
+        {
+            const common::SharedState<SP_ValueType>* state0 = state.Get();
+
+            assert(state0 != 0);
+
+            state0->Wait();
+        }
+
+        /**
+         * Wait for value to be set for specified time.
+         * Active thread will be blocked until value or error will be set or timeout will end.
+         *
+         * @param msTimeout Timeout in milliseconds.
+         * @return True if the object has been triggered and false in case of timeout.
+         */
+        bool WaitFor(int32_t msTimeout) const
+        {
+            const common::SharedState<SP_ValueType>* state0 = state.Get();
+
+            assert(state0 != 0);
+
+            return state0->WaitFor(msTimeout);
+        }
+
+        /**
+         * Get the set value.
+         * Active thread will be blocked until value or error will be set.
+         *
+         * @throw IgniteError if error has been set.
+         * @return Value that has been set on success.
+         */
+        SP_ValueType GetValue() const
+        {
+            const common::SharedState<SP_ValueType>* state0 = state.Get();
+
+            assert(state0 != 0);
+
+            return state0->GetValue();
+        }
+
+        /**
+         * Cancel related operation.
+         */
+        void Cancel()
+        {
+            common::SharedState<SP_ValueType>* state0 = state.Get();
+
+            assert(state0 != 0);
+
+            state0->Cancel();
+        }
+
+        /**
+         * Check if the future ready.
+         */
+        bool IsReady()
+        {
+            common::SharedState<SP_ValueType>* state0 = state.Get();
+
+            assert(state0 != 0);
+
+            return state0->IsSet();
+        }
+
+    private:
+        /**
+         * Constructor.
+         *
+         * @param state0 Shared state instance.
+         */
+        Future(common::concurrent::SharedPointer< common::SharedState<SP_ValueType> > state0) :
+            state(state0)
+        {
+            // No-op.
+        }
+
+        /** Shared state. */
+        common::concurrent::SharedPointer< common::SharedState<SP_ValueType> > state;
+    };
 }
 
 #endif //_IGNITE_FUTURE
diff --git a/modules/platforms/cpp/common/os/linux/include/ignite/common/common.h b/modules/platforms/cpp/common/os/linux/include/ignite/common/common.h
index 1698f65..6b7d960 100644
--- a/modules/platforms/cpp/common/os/linux/include/ignite/common/common.h
+++ b/modules/platforms/cpp/common/os/linux/include/ignite/common/common.h
@@ -53,4 +53,16 @@
     cls(const cls& src); \
     cls& operator= (const cls& other)
 
+#if (defined(__GNUC__) && (__GNUC__ > 7)) || __has_attribute(no_sanitize)
+#define IGNORE_SIGNED_OVERFLOW  __attribute__((no_sanitize("signed-integer-overflow")))
+#else
+#define IGNORE_SIGNED_OVERFLOW
+#endif
+
+#if (defined(__GNUC__) && (__GNUC__ > 7)) || __has_attribute(no_sanitize)
+#define IGNORE_FALSE_UNDEFINED __attribute__((no_sanitize("undefined")))
+#else
+#define IGNORE_FALSE_UNDEFINED
+#endif
+
 #endif //_IGNITE_COMMON_COMMON
diff --git a/modules/platforms/cpp/common/os/linux/include/ignite/common/concurrent_os.h b/modules/platforms/cpp/common/os/linux/include/ignite/common/concurrent_os.h
index 66f6656..c8f7293 100644
--- a/modules/platforms/cpp/common/os/linux/include/ignite/common/concurrent_os.h
+++ b/modules/platforms/cpp/common/os/linux/include/ignite/common/concurrent_os.h
@@ -691,6 +691,58 @@
                 /** State. */
                 bool state;
             };
+
+            /**
+             * Thread.
+             */
+            class IGNITE_IMPORT_EXPORT Thread
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                Thread();
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~Thread();
+
+                /**
+                 * Run thread.
+                 */
+                virtual void Run() = 0;
+
+                /**
+                 * Start thread.
+                 */
+                virtual void Start();
+
+                /**
+                 * Join thread.
+                 */
+                virtual void Join();
+
+            private:
+                IGNITE_NO_COPY_ASSIGNMENT(Thread);
+
+                /**
+                 * Routine.
+                 * @param lpParam Param.
+                 * @return Return code.
+                 */
+                static void* ThreadRoutine(void* arg);
+
+                /** Thread handle. */
+                pthread_t thread;
+            };
+
+            /**
+             * Get number of logical processors in system.
+             *
+             * @return Number of logical processors.
+             */
+            IGNITE_IMPORT_EXPORT uint32_t GetNumberOfProcessors();
         }
     }
 }
diff --git a/modules/platforms/cpp/common/os/linux/src/common/concurrent_os.cpp b/modules/platforms/cpp/common/os/linux/src/common/concurrent_os.cpp
index 7b48b9b..0370b57 100644
--- a/modules/platforms/cpp/common/os/linux/src/common/concurrent_os.cpp
+++ b/modules/platforms/cpp/common/os/linux/src/common/concurrent_os.cpp
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+#include <sys/sysinfo.h>
+
 #include "ignite/common/concurrent_os.h"
 
 namespace ignite
@@ -203,6 +205,46 @@
 
                 pthread_setspecific(tlsKey, ptr);
             }
+
+            Thread::Thread() :
+                thread()
+            {
+                // No-op.
+            }
+
+            Thread::~Thread()
+            {
+                // No-op.
+            }
+
+            void* Thread::ThreadRoutine(void* arg)
+            {
+                Thread* self = static_cast<Thread*>(arg);
+
+                self->Run();
+
+                return 0;
+            }
+
+            void Thread::Start()
+            {
+                int res = pthread_create(&thread, NULL, Thread::ThreadRoutine, this);
+
+                IGNITE_UNUSED(res);
+                assert(res == 0);
+            }
+
+            void Thread::Join()
+            {
+                pthread_join(thread, 0);
+            }
+
+            uint32_t GetNumberOfProcessors()
+            {
+                int res = get_nprocs();
+
+                return static_cast<uint32_t>(res < 0 ? 0 : res);
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/common/os/linux/src/common/platform_utils.cpp b/modules/platforms/cpp/common/os/linux/src/common/platform_utils.cpp
index 2f02d5b..ebea78b 100644
--- a/modules/platforms/cpp/common/os/linux/src/common/platform_utils.cpp
+++ b/modules/platforms/cpp/common/os/linux/src/common/platform_utils.cpp
@@ -23,8 +23,9 @@
 #include <dirent.h>
 #include <dlfcn.h>
 #include <glob.h>
-#include <unistd.h>
 #include <ftw.h>
+#include <unistd.h>
+#include <errno.h>
 
 #include <ignite/common/utils.h>
 
@@ -124,7 +125,7 @@
             return ostr;
         }
 
-        IGNITE_IMPORT_EXPORT unsigned GetRandSeed()
+        unsigned GetRandSeed()
         {
             timespec ts;
 
@@ -136,5 +137,22 @@
 
             return res;
         }
+
+        std::string GetLastSystemError()
+        {
+            int errorCode = errno;
+
+            std::string errorDetails;
+            if (errorCode != 0)
+            {
+                char errBuf[1024] = { 0 };
+
+                const char* res = strerror_r(errorCode, errBuf, sizeof(errBuf));
+                if (res)
+                    errorDetails.assign(res);
+            }
+
+            return errorDetails;
+        }
     }
 }
diff --git a/modules/platforms/cpp/common/os/win/include/ignite/common/common.h b/modules/platforms/cpp/common/os/win/include/ignite/common/common.h
index e7cda4b..c06f2c0 100644
--- a/modules/platforms/cpp/common/os/win/include/ignite/common/common.h
+++ b/modules/platforms/cpp/common/os/win/include/ignite/common/common.h
@@ -43,4 +43,7 @@
 
 #define IGNITE_UNUSED(x) ((void) x)
 
+#define IGNORE_SIGNED_OVERFLOW
+#define IGNORE_FALSE_UNDEFINED
+
 #endif //_IGNITE_COMMON_COMMON
\ No newline at end of file
diff --git a/modules/platforms/cpp/common/os/win/include/ignite/common/concurrent_os.h b/modules/platforms/cpp/common/os/win/include/ignite/common/concurrent_os.h
index b1e8916..25fa5c2 100644
--- a/modules/platforms/cpp/common/os/win/include/ignite/common/concurrent_os.h
+++ b/modules/platforms/cpp/common/os/win/include/ignite/common/concurrent_os.h
@@ -600,6 +600,58 @@
                 /** Event handle. */
                 HANDLE handle;
             };
+
+            /**
+             * Thread.
+             */
+            class IGNITE_IMPORT_EXPORT Thread
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                Thread();
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~Thread();
+
+                /**
+                 * Run thread.
+                 */
+                virtual void Run() = 0;
+
+                /**
+                 * Start thread.
+                 */
+                virtual void Start();
+
+                /**
+                 * Join thread.
+                 */
+                virtual void Join();
+
+            private:
+                IGNITE_NO_COPY_ASSIGNMENT(Thread);
+
+                /**
+                 * Routine.
+                 * @param lpParam Param.
+                 * @return Return code.
+                 */
+                static DWORD WINAPI ThreadRoutine(LPVOID lpParam);
+
+                /** Thread handle. */
+                HANDLE handle;
+            };
+
+            /**
+             * Get number of logical processors in system.
+             *
+             * @return Number of logical processors.
+             */
+            IGNITE_IMPORT_EXPORT uint32_t GetNumberOfProcessors();
         }
     }
 }
diff --git a/modules/platforms/cpp/common/os/win/src/common/concurrent_os.cpp b/modules/platforms/cpp/common/os/win/src/common/concurrent_os.cpp
index 15a015c..02b6834 100644
--- a/modules/platforms/cpp/common/os/win/src/common/concurrent_os.cpp
+++ b/modules/platforms/cpp/common/os/win/src/common/concurrent_os.cpp
@@ -207,6 +207,47 @@
             {
                 TlsSetValue(winTlsIdx, ptr);
             }
+
+            Thread::Thread() :
+                handle(NULL)
+            {
+                // No-op.
+            }
+
+            Thread::~Thread()
+            {
+                if (handle)
+                    CloseHandle(handle);
+            }
+
+            DWORD Thread::ThreadRoutine(LPVOID lpParam)
+            {
+                Thread* self = static_cast<Thread*>(lpParam);
+
+                self->Run();
+
+                return 0;
+            }
+
+            void Thread::Start()
+            {
+                handle = CreateThread(NULL, 0, Thread::ThreadRoutine, this, 0, NULL);
+
+                assert(handle != NULL);
+            }
+
+            void Thread::Join()
+            {
+                WaitForSingleObject(handle, INFINITE);
+            }
+
+            uint32_t GetNumberOfProcessors()
+            {
+                SYSTEM_INFO info;
+                GetSystemInfo(&info);
+
+                return static_cast<uint32_t>(info.dwNumberOfProcessors < 0 ? 0 : info.dwNumberOfProcessors);
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/common/os/win/src/common/platform_utils.cpp b/modules/platforms/cpp/common/os/win/src/common/platform_utils.cpp
index 9e2f68c..6141dd9 100644
--- a/modules/platforms/cpp/common/os/win/src/common/platform_utils.cpp
+++ b/modules/platforms/cpp/common/os/win/src/common/platform_utils.cpp
@@ -17,9 +17,11 @@
 
 #include <time.h>
 #include <vector>
+#include <sstream>
 
 #include <windows.h>
 
+#include <ignite/ignite_error.h>
 #include <ignite/common/platform_utils.h>
 
 namespace ignite
@@ -130,9 +132,28 @@
             return ostr;
         }
 
-        IGNITE_IMPORT_EXPORT unsigned GetRandSeed()
+        unsigned GetRandSeed()
         {
             return static_cast<unsigned>(GetTickCount() ^ GetCurrentProcessId());
         }
+
+        std::string GetLastSystemError()
+        {
+            DWORD errorCode = GetLastError();
+
+            std::string errorDetails;
+            if (errorCode != ERROR_SUCCESS)
+            {
+                char errBuf[1024] = { 0 };
+
+                FormatMessageA(
+                        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorCode,
+                        MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), errBuf, sizeof(errBuf), NULL);
+
+                errorDetails.assign(errBuf);
+            }
+
+            return errorDetails;
+        }
     }
 }
diff --git a/modules/platforms/cpp/common/src/common/big_integer.cpp b/modules/platforms/cpp/common/src/common/big_integer.cpp
index 9e3c80f..2c8b62d 100644
--- a/modules/platforms/cpp/common/src/common/big_integer.cpp
+++ b/modules/platforms/cpp/common/src/common/big_integer.cpp
@@ -199,7 +199,7 @@
         {
             if (val < 0)
             {
-                AssignUint64(static_cast<uint64_t>(-val));
+                AssignUint64(val > INT64_MIN ? static_cast<uint64_t>(-val) : static_cast<uint64_t>(val));
 
                 sign = -1;
             }
diff --git a/modules/platforms/cpp/common/src/common/bits.cpp b/modules/platforms/cpp/common/src/common/bits.cpp
index e2017a3..ec14c58 100644
--- a/modules/platforms/cpp/common/src/common/bits.cpp
+++ b/modules/platforms/cpp/common/src/common/bits.cpp
@@ -26,43 +26,43 @@
     {
         namespace bits
         {
+            int32_t NumberOfTrailingZerosU32(uint32_t i) {
+                int32_t c = 32;
+                if (!i)
+                    return c;
+
+#if defined(__GNUC__) || defined(__clang__)
+                return __builtin_ctz(i);
+#else
+                // See https://graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightParallel
+                // for details.
+                i &= ~i + 1u;
+
+                if (i)
+                    c--;
+
+                if (i & 0x0000FFFF)
+                    c -= 16;
+
+                if (i & 0x00FF00FF)
+                    c -= 8;
+
+                if (i & 0x0F0F0F0F)
+                    c -= 4;
+
+                if (i & 0x33333333)
+                    c -= 2;
+
+                if (i & 0x55555555)
+                    c -= 1;
+
+                return c;
+#endif
+            }
+
             int32_t NumberOfTrailingZerosI32(int32_t i)
             {
-                int32_t y;
-
-                if (i == 0) return 32;
-
-                int32_t n = 31;
-
-                y = i << 16;
-
-                if (y != 0) {
-                    n = n - 16;
-                    i = y;
-                }
-
-                y = i << 8; 
-
-                if (y != 0) {
-                    n = n - 8;
-                    i = y;
-                }
-
-                y = i << 4;
-
-                if (y != 0) {
-                    n = n - 4;
-                    i = y;
-                }
-
-                y = i << 2;
-
-                if (y != 0) {
-                    n = n - 2;
-                    i = y;
-                }
-
-                return n - static_cast<int32_t>(static_cast<uint32_t>(i << 1) >> 31);
+                return NumberOfTrailingZerosU32(static_cast<uint32_t>(i));
             }
 
             int32_t NumberOfLeadingZerosI32(int32_t i)
@@ -72,9 +72,12 @@
 
             int32_t NumberOfLeadingZerosU32(uint32_t i)
             {
-                if (i == 0)
+                if (!i)
                     return 32;
 
+#if defined(__GNUC__) || defined(__clang__)
+                return __builtin_clz(i);
+#else
                 int32_t n = 1;
 
                 if (i >> 16 == 0) {
@@ -98,6 +101,7 @@
                 }
 
                 return n - static_cast<int32_t>(i >> 31);
+#endif
             }
 
             int32_t NumberOfLeadingZerosI64(int64_t i)
@@ -107,8 +111,11 @@
 
             int32_t NumberOfLeadingZerosU64(uint64_t i)
             {
-                if (i == 0)
+                if (!i)
                     return 64;
+#if defined(__GNUC__) || defined(__clang__)
+                return __builtin_clzll(i);
+#else
 
                 int32_t n = 1;
 
@@ -139,13 +146,17 @@
                     x <<= 2;
                 }
 
-                n -= x >> 31;
+                n -= static_cast<int32_t>(x >> 31);
 
                 return n;
+#endif
             }
 
             int32_t BitCountI32(int32_t i)
             {
+#if defined(__GNUC__) || defined(__clang__)
+                return __builtin_popcount(i);
+#else
                 uint32_t ui = static_cast<uint32_t>(i);
 
                 ui -= (ui >> 1) & 0x55555555;
@@ -155,6 +166,7 @@
                 ui += ui >> 16;
 
                 return static_cast<int32_t>(ui & 0x3f);
+#endif
             }
 
             int32_t BitLengthI32(int32_t i)
diff --git a/modules/platforms/cpp/common/src/common/thread_pool.cpp b/modules/platforms/cpp/common/src/common/thread_pool.cpp
new file mode 100644
index 0000000..456c7eb
--- /dev/null
+++ b/modules/platforms/cpp/common/src/common/thread_pool.cpp
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+
+#include <exception>
+#include <iostream>
+
+#include <ignite/common/thread_pool.h>
+
+namespace ignite
+{
+    namespace common
+    {
+        ThreadPool::ThreadPool(uint32_t threadsNum):
+            started(false),
+            stopped(false),
+            threads()
+        {
+            uint32_t threadToStart = threadsNum != 0 ? threadsNum : concurrent::GetNumberOfProcessors();
+            if (!threadToStart)
+                threadToStart = 2;
+
+            threads.reserve(threadToStart);
+            for (uint32_t i = 0; i < threadToStart; ++i)
+            {
+                SP_WorkerThread thread(new WorkerThread(queue));
+                threads.push_back(thread);
+            }
+        }
+
+        ThreadPool::~ThreadPool()
+        {
+            Stop();
+        }
+
+        void ThreadPool::Start()
+        {
+            concurrent::CsLockGuard guard(mutex);
+
+            if (started)
+                return;
+
+            started = true;
+
+            for (std::vector<SP_WorkerThread>::iterator it = threads.begin(); it != threads.end(); ++it)
+                it->Get()->Start();
+        }
+
+        void ThreadPool::Stop()
+        {
+            concurrent::CsLockGuard guard(mutex);
+
+            if (stopped || !started)
+                return;
+
+            stopped = true;
+
+            queue.Unblock();
+
+            for (std::vector<SP_WorkerThread>::iterator it = threads.begin(); it != threads.end(); ++it)
+                it->Get()->Join();
+        }
+
+        void ThreadPool::Dispatch(const SP_ThreadPoolTask& task)
+        {
+            queue.Push(task);
+        }
+
+        ThreadPool::TaskQueue::TaskQueue() :
+            unblocked(false)
+        {
+            // No-op.
+        }
+
+        ThreadPool::TaskQueue::~TaskQueue()
+        {
+            // No-op.
+        }
+
+        void ThreadPool::TaskQueue::Push(const SP_ThreadPoolTask &task)
+        {
+            if (!task.IsValid())
+                return;
+
+            concurrent::CsLockGuard guard(mutex);
+
+            if (unblocked)
+            {
+                IgniteError err(IgniteError::IGNITE_ERR_GENERIC, "Execution thread pool is stopped");
+                HandleTaskError(*task.Get(), err);
+
+                return;
+            }
+
+            tasks.push_back(task);
+
+            waitPoint.NotifyOne();
+        }
+
+        SP_ThreadPoolTask ThreadPool::TaskQueue::Pop()
+        {
+            concurrent::CsLockGuard guard(mutex);
+            if (unblocked)
+                return SP_ThreadPoolTask();
+
+            while (tasks.empty())
+            {
+                waitPoint.Wait(mutex);
+
+                if (unblocked)
+                    return SP_ThreadPoolTask();
+            }
+
+            SP_ThreadPoolTask res = tasks.front();
+            tasks.pop_front();
+            return res;
+        }
+
+        void ThreadPool::TaskQueue::Unblock()
+        {
+            concurrent::CsLockGuard guard(mutex);
+            unblocked = true;
+
+            IgniteError err(IgniteError::IGNITE_ERR_GENERIC, "Execution thread pool is stopped");
+            for (std::deque< SP_ThreadPoolTask >::iterator it = tasks.begin(); it != tasks.end(); ++it)
+                HandleTaskError(*it->Get(), err);
+
+            tasks.clear();
+
+            waitPoint.NotifyAll();
+        }
+
+        ThreadPool::WorkerThread::WorkerThread(TaskQueue& taskQueue) :
+            taskQueue(taskQueue)
+        {
+            // No-op.
+        }
+
+        ThreadPool::WorkerThread::~WorkerThread()
+        {
+            // No-op.
+        }
+
+        void ThreadPool::WorkerThread::Run()
+        {
+            while (true)
+            {
+                SP_ThreadPoolTask task = taskQueue.Pop();
+
+                // Queue is unblocked and workers should stop.
+                if (!task.IsValid())
+                    break;
+
+                ThreadPoolTask &task0 = *task.Get();
+
+                try
+                {
+                    task0.Execute();
+                }
+                catch (const IgniteError& err)
+                {
+                    HandleTaskError(task0, err);
+                }
+                catch (const std::exception& err)
+                {
+                    IgniteError err0(IgniteError::IGNITE_ERR_STD, err.what());
+                    HandleTaskError(task0, err0);
+                }
+                catch (...)
+                {
+                    IgniteError err(IgniteError::IGNITE_ERR_UNKNOWN, "Unknown error occurred when executing task");
+                    HandleTaskError(task0, err);
+                }
+            }
+        }
+
+        void ThreadPool::HandleTaskError(ThreadPoolTask &task, const IgniteError &err)
+        {
+            try
+            {
+                task.OnError(err);
+
+                return;
+            }
+            catch (const IgniteError& err0)
+            {
+                std::cerr << "Exception is thrown during handling of exception: "
+                          << err0.what() << "Aborting execution" << std::endl;
+            }
+            catch (const std::exception& err0)
+            {
+                std::cerr << "Exception is thrown during handling of exception: "
+                          << err0.what() << "Aborting execution" << std::endl;
+            }
+            catch (...)
+            {
+                std::cerr << "Unknown exception is thrown during handling of exception. Aborting execution" << std::endl;
+            }
+
+            std::terminate();
+        }
+    }
+}
diff --git a/modules/platforms/cpp/common/src/common/utils.cpp b/modules/platforms/cpp/common/src/common/utils.cpp
index 6a5ee63..8e9d31b 100644
--- a/modules/platforms/cpp/common/src/common/utils.cpp
+++ b/modules/platforms/cpp/common/src/common/utils.cpp
@@ -15,6 +15,10 @@
  * limitations under the License.
  */
 
+#include <iomanip>
+#include <sstream>
+
+#include <ignite/ignite_error.h>
 #include <ignite/common/utils.h>
 
 namespace ignite
@@ -193,7 +197,7 @@
             return CTimeToTimestamp(localTime, ns);
         }
 
-        IGNITE_IMPORT_EXPORT std::string GetDynamicLibraryName(const char* name)
+        IGNITE_IMPORT_EXPORT std::string GetDynamicLibraryName(const std::string& name)
         {
             std::stringstream libNameBuffer;
 
@@ -212,5 +216,61 @@
             return i == val.end();
         }
 
+        std::string HexDump(const void *data, size_t count)
+        {
+            std::stringstream  dump;
+            size_t cnt = 0;
+            for(const uint8_t* p = (const uint8_t*)data, *e = (const uint8_t*)data + count; p != e; ++p)
+            {
+                if (cnt++ % 16 == 0)
+                {
+                    dump << std::endl;
+                }
+                dump << std::hex << std::setfill('0') << std::setw(2) << (int)*p << " ";
+            }
+            return dump.str();
+        }
+
+        void ThrowSystemError(const std::string &msg)
+        {
+            throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, msg.c_str());
+        }
+
+        void ThrowLastSystemError(const std::string& description, const std::string& advice)
+        {
+            ThrowSystemError(GetLastSystemError(description, advice));
+        }
+
+        void ThrowLastSystemError(const std::string& description)
+        {
+            std::string empty;
+            ThrowLastSystemError(description, empty);
+        }
+
+        std::string FormatErrorMessage(const std::string &description, const std::string &details,
+            const std::string &advice)
+        {
+            std::stringstream messageBuilder;
+            messageBuilder << description;
+            if (!details.empty())
+                messageBuilder << ": " << details;
+
+            if (!advice.empty())
+                messageBuilder << ". " << advice;
+
+            return messageBuilder.str();
+        }
+
+        std::string GetLastSystemError(const std::string& description, const std::string& advice)
+        {
+            return FormatErrorMessage(description, GetLastSystemError(), advice);
+        }
+
+        std::string GetLastSystemError(const std::string &description)
+        {
+            std::string empty;
+            return GetLastSystemError(description, empty);
+        }
+
     }
 }
diff --git a/modules/platforms/cpp/common/src/guid.cpp b/modules/platforms/cpp/common/src/guid.cpp
index df7c49f..4cb341e 100644
--- a/modules/platforms/cpp/common/src/guid.cpp
+++ b/modules/platforms/cpp/common/src/guid.cpp
@@ -60,10 +60,19 @@
 
     int64_t Guid::Compare(const Guid& other) const
     {
-        if (most != other.most)
-            return most - other.most;
+        if (most < other.most)
+            return -1;
 
-        return least - other.least;
+        if (most > other.most)
+            return 1;
+
+        if (least < other.least)
+            return -1;
+
+        if (least > other.least)
+            return 1;
+
+        return 0;
     }
     
     bool operator==(const Guid& val1, const Guid& val2)
diff --git a/modules/platforms/cpp/core-test/CMakeLists.txt b/modules/platforms/cpp/core-test/CMakeLists.txt
index 339da20..188716e 100644
--- a/modules/platforms/cpp/core-test/CMakeLists.txt
+++ b/modules/platforms/cpp/core-test/CMakeLists.txt
@@ -28,7 +28,8 @@
 include_directories(SYSTEM ${Boost_INCLUDE_DIRS} ${JNI_INCLUDE_DIRS})
 include_directories(include)
 
-set(SOURCES src/reference_test.cpp
+set(SOURCES
+        src/reference_test.cpp
         src/bits_test.cpp
         src/binary_identity_resolver_test.cpp
         src/cache_test.cpp
@@ -61,6 +62,7 @@
         src/test_utils.cpp
         src/cluster_node_test.cpp
         src/affinity_test.cpp
+        src/cache_query_schema_test.cpp
         src/cluster_group_test.cpp)
 
 add_executable(${TARGET} ${SOURCES})
@@ -77,5 +79,9 @@
 
 add_test(NAME ${TEST_TARGET} COMMAND ${TARGET} --catch_system_errors=no --log_level=all)
 
-set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT
-        IGNITE_NATIVE_TEST_CPP_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config)
+list(APPEND ENV_VARIABLES "IGNITE_NATIVE_TEST_CPP_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config")
+if (${WITH_SANITIZERS})
+    list(APPEND ENV_VARIABLES ${SANITIZERS_ENV})
+endif()
+
+set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT "${ENV_VARIABLES}")
diff --git a/modules/platforms/cpp/core-test/config/cache-query-schema-32.xml b/modules/platforms/cpp/core-test/config/cache-query-schema-32.xml
new file mode 100644
index 0000000..5c63912
--- /dev/null
+++ b/modules/platforms/cpp/core-test/config/cache-query-schema-32.xml
@@ -0,0 +1,52 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="cache-query-schema-default.xml"/>
+
+    <bean parent="grid.cfg">
+        <property name="memoryConfiguration">
+            <bean class="org.apache.ignite.configuration.MemoryConfiguration">
+                <property name="systemCacheInitialSize" value="#{10 * 1024 * 1024}"/>
+                <property name="systemCacheMaxSize" value="#{40 * 1024 * 1024}"/>
+                <property name="defaultMemoryPolicyName" value="dfltPlc"/>
+
+                <property name="memoryPolicies">
+                    <list>
+                        <bean class="org.apache.ignite.configuration.MemoryPolicyConfiguration">
+                            <property name="name" value="dfltPlc"/>
+                            <property name="maxSize" value="#{100 * 1024 * 1024}"/>
+                            <property name="initialSize" value="#{10 * 1024 * 1024}"/>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/core-test/config/cache-query-schema-default.xml b/modules/platforms/cpp/core-test/config/cache-query-schema-default.xml
new file mode 100644
index 0000000..84c12e1
--- /dev/null
+++ b/modules/platforms/cpp/core-test/config/cache-query-schema-default.xml
@@ -0,0 +1,60 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <bean abstract="true" id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <property name="localHost" value="127.0.0.1"/>
+        <property name="connectorConfiguration"><null/></property>
+
+        <property name="sqlSchemas">
+            <list>
+                <value>SCHEMA_1</value>
+                <value>SCHEMA_2</value>
+                <value>"ScHeMa3"</value>
+                <value>SCHEMA_4</value>
+            </list>
+        </property>
+
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+                <property name="socketTimeout" value="300" />
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/core-test/config/cache-query-schema.xml b/modules/platforms/cpp/core-test/config/cache-query-schema.xml
new file mode 100644
index 0000000..5e13ac4
--- /dev/null
+++ b/modules/platforms/cpp/core-test/config/cache-query-schema.xml
@@ -0,0 +1,34 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="cache-query-schema-default.xml"/>
+
+    <bean parent="grid.cfg"/>
+</beans>
diff --git a/modules/platforms/cpp/core-test/src/cache_query_schema_test.cpp b/modules/platforms/cpp/core-test/src/cache_query_schema_test.cpp
new file mode 100644
index 0000000..b796464
--- /dev/null
+++ b/modules/platforms/cpp/core-test/src/cache_query_schema_test.cpp
@@ -0,0 +1,312 @@
+/*
+ * 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.
+ */
+
+#include <stdint.h>
+
+#include <iterator>
+
+#include <boost/test/unit_test.hpp>
+
+#include <ignite/common/utils.h>
+
+#include "ignite/cache/cache.h"
+#include "ignite/cache/query/query_cursor.h"
+#include "ignite/cache/query/query_sql_fields.h"
+#include "ignite/ignite.h"
+#include "ignite/ignition.h"
+#include "ignite/test_utils.h"
+
+using namespace boost::unit_test;
+
+using namespace ignite;
+using namespace ignite::cache;
+using namespace ignite::cache::query;
+using namespace ignite::common;
+
+using ignite::impl::binary::BinaryUtils;
+
+/**
+ * Ensure that cursor is empy fails.
+ *
+ * @param cur Cursor.
+ */
+template<typename Cursor>
+void ChechEmptyCursorGetNextThrowsException(Cursor& cur)
+{
+    BOOST_REQUIRE(!cur.HasNext());
+    BOOST_CHECK_EXCEPTION(cur.GetNext(), IgniteError, ignite_test::IsGenericError);
+}
+
+/**
+ * Check single row through iteration.
+ *
+ * @param cur Cursor.
+ * @param c1 First column.
+ */
+template<typename T1>
+void CheckSingleRow(QueryFieldsCursor& cur, const T1& c1)
+{
+    BOOST_REQUIRE(cur.HasNext());
+
+    QueryFieldsRow row = cur.GetNext();
+
+    BOOST_REQUIRE_EQUAL(row.GetNext<T1>(), c1);
+
+    BOOST_REQUIRE(!row.HasNext());
+
+    ChechEmptyCursorGetNextThrowsException(cur);
+}
+
+/**
+ * Check row through iteration.
+ *
+ * @param cur Cursor.
+ * @param c1 First column.
+ */
+template<typename T1, typename T2>
+void CheckRow(QueryFieldsCursor& cur, const T1& c1, const T2& c2)
+{
+    BOOST_REQUIRE(cur.HasNext());
+
+    QueryFieldsRow row = cur.GetNext();
+
+    BOOST_REQUIRE_EQUAL(row.GetNext<T1>(), c1);
+    BOOST_REQUIRE_EQUAL(row.GetNext<T2>(), c2);
+
+    BOOST_REQUIRE(!row.HasNext());
+}
+
+/**
+ * Check single row through iteration.
+ *
+ * @param cur Cursor.
+ * @param c1 First column.
+ */
+template<typename T1, typename T2>
+void CheckSingleRow(QueryFieldsCursor& cur, const T1& c1, const T2& c2)
+{
+    CheckRow<T1, T2>(cur, c1, c2);
+
+    ChechEmptyCursorGetNextThrowsException(cur);
+}
+
+static const std::string TABLE_NAME = "T1";
+static const std::string SCHEMA_NAME_1 = "SCHEMA_1";
+static const std::string SCHEMA_NAME_2 = "SCHEMA_2";
+static const std::string SCHEMA_NAME_3 = "ScHeMa3";
+static const std::string Q_SCHEMA_NAME_3 = '"' + SCHEMA_NAME_3 + '"';
+static const std::string SCHEMA_NAME_4 = "SCHEMA_4";
+
+/**
+ * Test setup fixture.
+ */
+struct CacheQuerySchemaTestSuiteFixture
+{
+    Ignite StartNode(const char* name)
+    {
+#ifdef IGNITE_TESTS_32
+        return ignite_test::StartNode("cache-query-schema-32.xml", name);
+#else
+        return ignite_test::StartNode("cache-query-schema.xml", name);
+#endif
+    }
+
+    /**
+     * Constructor.
+     */
+    CacheQuerySchemaTestSuiteFixture() :
+        grid(StartNode("Node1"))
+    {
+        // No-op.
+    }
+
+    /**
+     * Destructor.
+     */
+    ~CacheQuerySchemaTestSuiteFixture()
+    {
+        Ignition::StopAll(true);
+    }
+
+    /** Perform SQL in cluster. */
+    QueryFieldsCursor Sql(const std::string& sql)
+    {
+        return grid
+            .GetOrCreateCache<int, int>("SchemaTestCache")
+            .Query(SqlFieldsQuery(sql));
+    }
+
+    std::string TableName(bool withSchema)
+    {
+        return withSchema ? "PUBLIC." + TABLE_NAME : TABLE_NAME;
+    }
+
+    template<typename Predicate>
+    void ExecuteStatementsAndVerify(Predicate& pred)
+    {
+        Sql("CREATE TABLE " + TableName(pred()) + " (id INT PRIMARY KEY, val INT)");
+
+        Sql("CREATE INDEX t1_idx_1 ON " + TableName(pred()) + "(val)");
+
+        Sql("INSERT INTO " + TableName(pred()) + " (id, val) VALUES(1, 2)");
+
+        QueryFieldsCursor cursor = Sql("SELECT * FROM " + TableName(pred()));
+        CheckSingleRow<int32_t, int32_t>(cursor, 1, 2);
+
+        Sql("UPDATE " + TableName(pred()) + " SET val = 5");
+        cursor = Sql("SELECT * FROM " + TableName(pred()));
+        CheckSingleRow<int32_t, int32_t>(cursor, 1, 5);
+
+        Sql("DELETE FROM " + TableName(pred()) + " WHERE id = 1");
+        cursor = Sql("SELECT COUNT(*) FROM " + TableName(pred()));
+        CheckSingleRow<int64_t>(cursor, 0);
+
+        cursor = Sql("SELECT COUNT(*) FROM SYS.TABLES WHERE schema_name = 'PUBLIC' "
+            "AND table_name = \'" + TABLE_NAME + "\'");
+        CheckSingleRow<int64_t>(cursor, 1);
+
+        Sql("DROP TABLE " + TableName(pred()));
+    }
+
+    /** Node started during the test. */
+    Ignite grid;
+};
+
+BOOST_FIXTURE_TEST_SUITE(CacheQuerySchemaTestSuite, CacheQuerySchemaTestSuiteFixture)
+
+bool TruePred()
+{
+    return true;
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsExplicitPublicSchema)
+{
+    ExecuteStatementsAndVerify(TruePred);
+}
+
+bool FalsePred()
+{
+    return false;
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsImplicitPublicSchema)
+{
+    ExecuteStatementsAndVerify(FalsePred);
+}
+
+struct MixedPred
+{
+    int i;
+
+    MixedPred() : i(0)
+    {
+        // No-op.
+    }
+
+    bool operator()()
+    {
+        return (++i & 1) == 0;
+    }
+};
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsMixedPublicSchema)
+{
+    MixedPred pred;
+
+    ExecuteStatementsAndVerify(pred);
+}
+
+BOOST_AUTO_TEST_CASE(TestCreateDropNonExistingSchema)
+{
+    BOOST_CHECK_THROW(
+        Sql("CREATE TABLE UNKNOWN_SCHEMA." + TABLE_NAME + "(id INT PRIMARY KEY, val INT)"),
+        IgniteError
+    );
+
+    BOOST_CHECK_THROW(
+        Sql("DROP TABLE UNKNOWN_SCHEMA." + TABLE_NAME),
+        IgniteError
+    );
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsDiffSchemas)
+{
+    Sql("CREATE TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " (s1_key INT PRIMARY KEY, s1_val INT)");
+    Sql("CREATE TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " (s2_key INT PRIMARY KEY, s2_val INT)");
+    Sql("CREATE TABLE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " (s3_key INT PRIMARY KEY, s3_val INT)");
+    Sql("CREATE TABLE " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " (s4_key INT PRIMARY KEY, s4_val INT)");
+
+    Sql("INSERT INTO " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " (s1_key, s1_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " (s2_key, s2_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " (s3_key, s3_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " (s4_key, s4_val) VALUES (1, 2)");
+
+    Sql("UPDATE " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " SET s1_val = 5");
+    Sql("UPDATE " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " SET s2_val = 5");
+    Sql("UPDATE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " SET s3_val = 5");
+    Sql("UPDATE " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " SET s4_val = 5");
+
+    Sql("DELETE FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_1 + '.' + TABLE_NAME + "(s1_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_2 + '.' + TABLE_NAME + "(s2_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + "(s3_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_4 + '.' + TABLE_NAME + "(s4_val)");
+
+    Sql("SELECT * FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    Sql("SELECT * FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME
+      + " JOIN " + SCHEMA_NAME_2 + '.' + TABLE_NAME
+      + " JOIN " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME
+      + " JOIN " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    QueryFieldsCursor cursor = Sql("SELECT SCHEMA_NAME, KEY_ALIAS FROM SYS.TABLES ORDER BY SCHEMA_NAME");
+
+    CheckRow<std::string, std::string>(cursor, SCHEMA_NAME_1, "S1_KEY");
+    CheckRow<std::string, std::string>(cursor, SCHEMA_NAME_2, "S2_KEY");
+    CheckRow<std::string, std::string>(cursor, SCHEMA_NAME_4, "S4_KEY");
+    CheckRow<std::string, std::string>(cursor, SCHEMA_NAME_3, "S3_KEY");
+
+    ChechEmptyCursorGetNextThrowsException(cursor);
+
+    Sql("DROP TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+}
+
+BOOST_AUTO_TEST_CASE(TestCreateTblsInDiffSchemasForSameCache)
+{
+    std::string testCache = "cache1";
+
+    Sql("CREATE TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME +
+        " (s1_key INT PRIMARY KEY, s1_val INT) WITH \"cache_name=" + testCache + '"');
+
+    BOOST_CHECK_THROW(
+        Sql("CREATE TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME +
+            " (s1_key INT PRIMARY KEY, s2_val INT) WITH \"cache_name=" + testCache + '"'),
+        IgniteError
+    );
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/core-test/src/continuous_query_test.cpp b/modules/platforms/cpp/core-test/src/continuous_query_test.cpp
index f102222..a6715b9 100644
--- a/modules/platforms/cpp/core-test/src/continuous_query_test.cpp
+++ b/modules/platforms/cpp/core-test/src/continuous_query_test.cpp
@@ -133,8 +133,9 @@
      * @param key Key.
      * @param oldVal Old value.
      * @param val Current value.
+     * @param eType Evenet type.
      */
-    void CheckNextEvent(const K& key, boost::optional<V> oldVal, boost::optional<V> val)
+    void CheckNextEvent(const K& key, boost::optional<V> oldVal, boost::optional<V> val, CacheEntryEventType::T eType)
     {
         CacheEntryEvent<K, V> event;
         bool success = eventQueue.Pull(event, boost::chrono::seconds(1));
@@ -144,6 +145,7 @@
         BOOST_CHECK_EQUAL(event.GetKey(), key);
         BOOST_CHECK_EQUAL(event.HasOldValue(), oldVal.is_initialized());
         BOOST_CHECK_EQUAL(event.HasValue(), val.is_initialized());
+        BOOST_CHECK_EQUAL(event.GetEventType(), eType);
 
         if (oldVal && event.HasOldValue())
             BOOST_CHECK_EQUAL(event.GetOldValue().value, oldVal->value);
@@ -334,16 +336,16 @@
 void CheckEvents(Cache<int, TestEntry>& cache, Listener<int, TestEntry>& lsnr)
 {
     cache.Put(1, TestEntry(10));
-    lsnr.CheckNextEvent(1, boost::none, TestEntry(10));
+    lsnr.CheckNextEvent(1, boost::none, TestEntry(10), CacheEntryEventType::CREATE);
 
     cache.Put(1, TestEntry(20));
-    lsnr.CheckNextEvent(1, TestEntry(10), TestEntry(20));
+    lsnr.CheckNextEvent(1, TestEntry(10), TestEntry(20), CacheEntryEventType::UPDATE);
 
     cache.Put(2, TestEntry(20));
-    lsnr.CheckNextEvent(2, boost::none, TestEntry(20));
+    lsnr.CheckNextEvent(2, boost::none, TestEntry(20), CacheEntryEventType::CREATE);
 
     cache.Remove(1);
-    lsnr.CheckNextEvent(1, TestEntry(20), TestEntry(20));
+    lsnr.CheckNextEvent(1, TestEntry(20), TestEntry(20), CacheEntryEventType::REMOVE);
 }
 
 IGNITE_EXPORTED_CALL void IgniteModuleInit0(ignite::IgniteBindingContext& context)
@@ -684,14 +686,14 @@
     cache.Put(150, TestEntry(1502));
     cache.Remove(150);
 
-    lsnr.CheckNextEvent(100, boost::none, TestEntry(1000));
-    lsnr.CheckNextEvent(101, boost::none, TestEntry(1010));
+    lsnr.CheckNextEvent(100, boost::none, TestEntry(1000), CacheEntryEventType::CREATE);
+    lsnr.CheckNextEvent(101, boost::none, TestEntry(1010), CacheEntryEventType::CREATE);
 
-    lsnr.CheckNextEvent(142, boost::none, TestEntry(1420));
-    lsnr.CheckNextEvent(142, TestEntry(1420), TestEntry(1421));
-    lsnr.CheckNextEvent(142, TestEntry(1421), TestEntry(1421));
+    lsnr.CheckNextEvent(142, boost::none, TestEntry(1420), CacheEntryEventType::CREATE);
+    lsnr.CheckNextEvent(142, TestEntry(1420), TestEntry(1421), CacheEntryEventType::UPDATE);
+    lsnr.CheckNextEvent(142, TestEntry(1421), TestEntry(1421), CacheEntryEventType::REMOVE);
 
-    lsnr.CheckNextEvent(149, boost::none, TestEntry(1490));
+    lsnr.CheckNextEvent(149, boost::none, TestEntry(1490), CacheEntryEventType::CREATE);
 }
 
 BOOST_AUTO_TEST_CASE(TestFilterMultipleNodes)
@@ -734,14 +736,14 @@
     for (int i = 200; i < 250; ++i)
         cache2.Put(i, TestEntry(i * 10));
 
-    lsnr.CheckNextEvent(100, boost::none, TestEntry(1000));
-    lsnr.CheckNextEvent(101, boost::none, TestEntry(1010));
+    lsnr.CheckNextEvent(100, boost::none, TestEntry(1000), CacheEntryEventType::CREATE);
+    lsnr.CheckNextEvent(101, boost::none, TestEntry(1010), CacheEntryEventType::CREATE);
 
-    lsnr.CheckNextEvent(142, boost::none, TestEntry(1420));
-    lsnr.CheckNextEvent(142, TestEntry(1420), TestEntry(1421));
-    lsnr.CheckNextEvent(142, TestEntry(1421), TestEntry(1421));
+    lsnr.CheckNextEvent(142, boost::none, TestEntry(1420), CacheEntryEventType::CREATE);
+    lsnr.CheckNextEvent(142, TestEntry(1420), TestEntry(1421), CacheEntryEventType::UPDATE);
+    lsnr.CheckNextEvent(142, TestEntry(1421), TestEntry(1421), CacheEntryEventType::REMOVE);
 
-    lsnr.CheckNextEvent(149, boost::none, TestEntry(1490));
+    lsnr.CheckNextEvent(149, boost::none, TestEntry(1490), CacheEntryEventType::CREATE);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/core/include/ignite/cache/event/cache_entry_event.h b/modules/platforms/cpp/core/include/ignite/cache/event/cache_entry_event.h
index 0a48c71..0323eed 100644
--- a/modules/platforms/cpp/core/include/ignite/cache/event/cache_entry_event.h
+++ b/modules/platforms/cpp/core/include/ignite/cache/event/cache_entry_event.h
@@ -31,6 +31,45 @@
     namespace cache
     {
         /**
+         * Cache entry event type.
+         */
+        struct CacheEntryEventType
+        {
+            enum T
+            {
+                /** An event type indicating that the cache entry was created. */
+                CREATE = 0,
+
+                /** An event type indicating that the cache entry was updated. i.e. a previous */
+                UPDATE = 1,
+
+                /** An event type indicating that the cache entry was removed. */
+                REMOVE = 2,
+
+                /** An event type indicating that the cache entry was removed by expiration policy. */
+                EXPIRE = 3
+            };
+
+            static T FromInt8(int8_t val)
+            {
+                switch (val)
+                {
+                    case CREATE:
+                    case UPDATE:
+                    case REMOVE:
+                    case EXPIRE:
+                        return static_cast<T>(val);
+
+                    default:
+                    {
+                        IGNITE_ERROR_FORMATTED_1(IgniteError::IGNITE_ERR_BINARY,
+                            "Unsupported CacheEntryEventType", "val", val);
+                    }
+                }
+            }
+        };
+
+        /**
          * Cache entry event class template.
          *
          * Both key and value types should be default-constructable,
@@ -48,7 +87,8 @@
             CacheEntryEvent() :
                 CacheEntry<K, V>(),
                 oldVal(),
-                hasOldValue(false)
+                hasOldValue(false),
+                eventType(CacheEntryEventType::CREATE)
             {
                 // No-op.
             }
@@ -61,7 +101,8 @@
             CacheEntryEvent(const CacheEntryEvent<K, V>& other) :
                 CacheEntry<K, V>(other),
                 oldVal(other.oldVal),
-                hasOldValue(other.hasOldValue)
+                hasOldValue(other.hasOldValue),
+                eventType(other.eventType)
             {
                 // No-op.
             }
@@ -88,6 +129,7 @@
 
                     oldVal = other.oldVal;
                     hasOldValue = other.hasOldValue;
+                    eventType = other.eventType;
                 }
 
                 return *this;
@@ -114,6 +156,18 @@
             }
 
             /**
+             * Get event type.
+             *
+             * @see CacheEntryEventType::T for details on possible types of events.
+             *
+             * @return Event type.
+             */
+            CacheEntryEventType::T GetEventType() const
+            {
+                return eventType;
+            }
+
+            /**
              * Reads cache event using provided raw reader.
              *
              * @param reader Reader to use.
@@ -124,9 +178,9 @@
 
                 this->hasOldValue = reader.TryReadObject(this->oldVal);
                 this->hasValue = reader.TryReadObject(this->val);
-                
-                // Java send an event type, we need to fetch it from the buffer.
-                reader.ReadInt8();
+
+                int8_t eventTypeByte = reader.ReadInt8();
+                this->eventType = CacheEntryEventType::FromInt8(eventTypeByte);
             }
 
         private:
@@ -135,6 +189,9 @@
 
             /** Indicates whether old value exists */
             bool hasOldValue;
+
+            /** Event type. */
+            CacheEntryEventType::T eventType;
         };
     }
 }
diff --git a/modules/platforms/cpp/core/include/ignite/impl/cache/query/continuous/continuous_query_impl.h b/modules/platforms/cpp/core/include/ignite/impl/cache/query/continuous/continuous_query_impl.h
index d2bf241..ce1a46e 100644
--- a/modules/platforms/cpp/core/include/ignite/impl/cache/query/continuous/continuous_query_impl.h
+++ b/modules/platforms/cpp/core/include/ignite/impl/cache/query/continuous/continuous_query_impl.h
@@ -158,7 +158,7 @@
                          * means that time check is disabled and entries will be
                          * sent only when buffer is full.
                          *
-                         * @param val Time interval in miliseconds.
+                         * @param val Time interval in milliseconds.
                          */
                         void SetTimeInterval(int64_t val)
                         {
@@ -223,7 +223,7 @@
                         int32_t bufferSize;
 
                         /**
-                         * Time interval in miliseconds. When a cache update
+                         * Time interval in milliseconds. When a cache update
                          * happens, entry is first put into a buffer. Entries from
                          * buffer will be sent to the master node only if the buffer
                          * is full (its size can be changed via SetBufferSize) or
@@ -374,7 +374,7 @@
                          * Constructor.
                          */
                         template<typename F>
-                        RemoteFilterHolder(const Reference<F>& filter):
+                        explicit RemoteFilterHolder(const Reference<F>& filter):
                             ContinuousQueryImplBase(false, new event::CacheEntryEventFilterHolder<F>(filter))
                         {
                             // No-op.
diff --git a/modules/platforms/cpp/examples/CMakeLists.txt b/modules/platforms/cpp/examples/CMakeLists.txt
index 8c635f5..7a47785 100644
--- a/modules/platforms/cpp/examples/CMakeLists.txt
+++ b/modules/platforms/cpp/examples/CMakeLists.txt
@@ -30,6 +30,12 @@
     set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
 endif()
 
+option (WITH_SANITIZERS "Build with sanitizers" OFF)
+
+if (${WITH_SANITIZERS} AND (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU"))
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -g")
+endif()
+
 add_subdirectory(compute-example)
 add_subdirectory(continuous-query-example)
 add_subdirectory(odbc-example)
diff --git a/modules/platforms/cpp/network/CMakeLists.txt b/modules/platforms/cpp/network/CMakeLists.txt
index e8e9703..42af167 100644
--- a/modules/platforms/cpp/network/CMakeLists.txt
+++ b/modules/platforms/cpp/network/CMakeLists.txt
@@ -23,20 +23,38 @@
 
 include_directories(include src ${OPENSSL_INCLUDE_DIR})
 
-set(SOURCES src/network/network.cpp
+set(SOURCES
+        src/network/async_client_pool_adapter.cpp
+        src/network/error_handling_filter.cpp
+        src/network/codec_data_filter.cpp
+        src/network/data_buffer.cpp
+        src/network/length_prefix_codec.cpp
+        src/network/network.cpp
+        src/network/ssl/secure_data_filter.cpp
         src/network/ssl/secure_socket_client.cpp
+        src/network/ssl/secure_utils.cpp
         src/network/ssl/ssl_gateway.cpp)
 
 if (WIN32)
     include_directories(os/win/src)
 
-    list(APPEND SOURCES os/win/src/network/tcp_socket_client.cpp
+    list(APPEND SOURCES
+            os/win/src/network/tcp_socket_client.cpp
             os/win/src/network/sockets.cpp
-            os/win/src/network/utils.cpp)
+            os/win/src/network/utils.cpp
+            os/win/src/network/win_async_client.cpp
+            os/win/src/network/win_async_client_pool.cpp
+            os/win/src/network/win_async_connecting_thread.cpp
+            os/win/src/network/win_async_worker_thread.cpp)
 else()
     include_directories(os/linux/src)
 
-    list(APPEND SOURCES os/linux/src/network/tcp_socket_client.cpp
+    list(APPEND SOURCES
+            os/linux/src/network/connecting_context.cpp
+            os/linux/src/network/linux_async_client.cpp
+            os/linux/src/network/linux_async_client_pool.cpp
+            os/linux/src/network/linux_async_worker_thread.cpp
+            os/linux/src/network/tcp_socket_client.cpp
             os/linux/src/network/sockets.cpp
             os/linux/src/network/utils.cpp)
 endif()
@@ -60,13 +78,13 @@
     if (${_target_lib} STREQUAL ${TARGET}-objlib)
         set_target_properties(${_target_lib} PROPERTIES POSITION_INDEPENDENT_CODE 1)
 
-        target_link_libraries(${_target_lib} ignite-common-objlib)
+        target_link_libraries(${_target_lib} ignite-common-objlib ignite-binary-objlib)
     else()
-        target_link_libraries(${_target_lib} ignite-common)
+        target_link_libraries(${_target_lib} ignite-common ignite-binary)
     endif()
 
     if (WIN32)
-        target_link_libraries(${_target_lib} wsock32 ws2_32 iphlpapi)
+        target_link_libraries(${_target_lib} wsock32 ws2_32 iphlpapi crypt32)
     endif()
 
     target_include_directories(${_target_lib} INTERFACE include ${OS_INCLUDE})
diff --git a/modules/platforms/cpp/network/include/ignite/network/async_client_pool.h b/modules/platforms/cpp/network/include/ignite/network/async_client_pool.h
new file mode 100644
index 0000000..9ecf20e
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/async_client_pool.h
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_ASYNC_CLIENT_POOL
+#define _IGNITE_NETWORK_ASYNC_CLIENT_POOL
+
+#include <stdint.h>
+
+#include <vector>
+
+#include <ignite/ignite_error.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_handler.h>
+#include <ignite/network/data_filter.h>
+#include <ignite/network/data_sink.h>
+#include <ignite/network/tcp_range.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Asynchronous client pool.
+         */
+        class IGNITE_IMPORT_EXPORT AsyncClientPool : public DataSink
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~AsyncClientPool()
+            {
+                // No-op.
+            }
+
+            /**
+             * Start internal thread that establishes connections to provided addresses and asynchronously sends and
+             * receives messages from them. Function returns either when thread is started and first connection is
+             * established or failure happened.
+             *
+             * @param addrs Addresses to connect to.
+             * @param connLimit Connection upper limit. Zero means limit is disabled.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual void Start(const std::vector<TcpRange>& addrs, uint32_t connLimit) = 0;
+
+            /**
+             * Close all established connections and stops handling threads.
+             */
+            virtual void Stop() = 0;
+
+            /**
+             * Set handler.
+             *
+             * @param handler Handler to set.
+             */
+            virtual void SetHandler(AsyncHandler *handler) = 0;
+        };
+
+        // Type alias
+        typedef common::concurrent::SharedPointer<AsyncClientPool> SP_AsyncClientPool;
+    }
+}
+
+#endif //_IGNITE_NETWORK_ASYNC_CLIENT_POOL
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/include/ignite/network/async_handler.h b/modules/platforms/cpp/network/include/ignite/network/async_handler.h
new file mode 100644
index 0000000..ecb1d36
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/async_handler.h
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_ASYNC_HANDLER
+#define _IGNITE_NETWORK_ASYNC_HANDLER
+
+#include <stdint.h>
+
+#include <ignite/ignite_error.h>
+#include <ignite/network/end_point.h>
+#include <ignite/network/data_buffer.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Asynchronous events handler.
+         */
+        class IGNITE_IMPORT_EXPORT AsyncHandler
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~AsyncHandler()
+            {
+                // No-op.
+            }
+
+            /**
+             * Callback that called on successful connection establishment.
+             *
+             * @param addr Address of the new connection.
+             * @param id Connection ID.
+             */
+            virtual void OnConnectionSuccess(const EndPoint& addr, uint64_t id) = 0;
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param addr Connection address.
+             * @param err Error.
+             */
+            virtual void OnConnectionError(const EndPoint& addr, const IgniteError& err) = 0;
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            virtual void OnConnectionClosed(uint64_t id, const IgniteError* err) = 0;
+
+            /**
+             * Callback that called when new message is received.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            virtual void OnMessageReceived(uint64_t id, const DataBuffer& msg) = 0;
+
+            /**
+             * Callback that called when message is sent.
+             *
+             * @param id Async client ID.
+             */
+            virtual void OnMessageSent(uint64_t id) = 0;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_ASYNC_HANDLER
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/include/ignite/network/codec.h b/modules/platforms/cpp/network/include/ignite/network/codec.h
new file mode 100644
index 0000000..b8938c0
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/codec.h
@@ -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.
+ */
+
+#ifndef _IGNITE_NETWORK_CODEC
+#define _IGNITE_NETWORK_CODEC
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/factory.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/data_buffer.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Codec class.
+         * Encodes and decodes data.
+         */
+        class IGNITE_IMPORT_EXPORT Codec
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~Codec()
+            {
+                // No-op.
+            }
+
+            /**
+             * Encode provided data.
+             *
+             * @param data Data to encode.
+             * @return Encoded data. Returning null is ok.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual DataBuffer Encode(DataBuffer& data) = 0;
+
+            /**
+             * Decode provided data.
+             *
+             * @param data Data to decode.
+             * @return Decoded data. Returning null means data is not yet ready.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual DataBuffer Decode(DataBuffer& data) = 0;
+        };
+
+        // Shared pointer codec type alias.
+        typedef common::concurrent::SharedPointer<Codec> SP_Codec;
+
+        /** Codec factory. */
+        typedef common::Factory<Codec> CodecFactory;
+
+        // Shared pointer to codec factory type alias.
+        typedef common::concurrent::SharedPointer<CodecFactory> SP_CodecFactory;
+    }
+}
+
+#endif //_IGNITE_NETWORK_CODEC
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/include/ignite/network/codec_data_filter.h b/modules/platforms/cpp/network/include/ignite/network/codec_data_filter.h
new file mode 100644
index 0000000..feaeddb
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/codec_data_filter.h
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_CODEC_DATA_FILTER
+#define _IGNITE_NETWORK_CODEC_DATA_FILTER
+
+#include <map>
+
+#include <ignite/common/concurrent.h>
+
+#include <ignite/network/codec.h>
+#include <ignite/network/data_filter_adapter.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Data filter that uses codecs inside to encode/decode data.
+         */
+        class IGNITE_IMPORT_EXPORT CodecDataFilter : public DataFilterAdapter
+        {
+        public:
+            /**
+             * Constructor.
+             *
+             * @param factory Codec factory.
+             */
+            explicit CodecDataFilter(const SP_CodecFactory& factory);
+
+            /**
+             * Destructor.
+             */
+            virtual ~CodecDataFilter();
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data);
+
+            /**
+              * Callback that called on successful connection establishment.
+              *
+              * @param addr Address of the new connection.
+              * @param id Connection ID.
+              */
+            virtual void OnConnectionSuccess(const EndPoint& addr, uint64_t id);
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            virtual void OnConnectionClosed(uint64_t id, const IgniteError* err);
+
+            /**
+             * Callback that called when new message is received.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            virtual void OnMessageReceived(uint64_t id, const DataBuffer& msg);
+
+        private:
+            /** Codec map. */
+            typedef std::map<uint64_t, SP_Codec> CodecMap;
+
+            /**
+             * Get codec for connection.
+             *
+             * @param id Connection ID.
+             * @return Codec if found or null.
+             */
+            SP_Codec FindCodec(uint64_t id);
+
+            /** Codec factory. */
+            SP_CodecFactory codecFactory;
+
+            /** Codecs. */
+            CodecMap* codecs;
+
+            /** Mutex for secure access to codecs map. */
+            common::concurrent::CriticalSection codecsCs;
+        };
+
+        // Shared pointer type alias.
+        typedef common::concurrent::SharedPointer<CodecDataFilter> SP_CodecDataFilter;
+    }
+}
+
+#endif //_IGNITE_NETWORK_CODEC_DATA_FILTER
diff --git a/modules/platforms/cpp/network/include/ignite/network/data_buffer.h b/modules/platforms/cpp/network/include/ignite/network/data_buffer.h
new file mode 100644
index 0000000..cf2a3d9
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/data_buffer.h
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_DATA_BUFFER
+#define _IGNITE_NETWORK_DATA_BUFFER
+
+#include <ignite/impl/interop/interop_memory.h>
+#include <ignite/impl/interop/interop_input_stream.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Data buffer.
+         */
+        class IGNITE_IMPORT_EXPORT DataBuffer
+        {
+        public:
+            /**
+             * Default constructor.
+             */
+            DataBuffer();
+
+            /**
+             * Constructor.
+             *
+             * @param data Data.
+             */
+            explicit DataBuffer(const impl::interop::SP_ConstInteropMemory& data);
+
+            /**
+             * Constructor.
+             *
+             * @param data Data.
+             * @param pos Start of data.
+             * @param len Length.
+             */
+            DataBuffer(const impl::interop::SP_ConstInteropMemory& data, int32_t pos, int32_t len);
+
+            /**
+             * Destructor.
+             */
+            ~DataBuffer()
+            {
+                // No-op.
+            }
+
+            /**
+             * Consume data from buffer to specified place in memory.
+             *
+             * @param dst Destination in memory.
+             * @param size Number of bytes to copy.
+             */
+            void Consume(int8_t* dst, int32_t size);
+
+            /**
+             * Get data pointer.
+             *
+             * @return Data.
+             */
+            const int8_t* GetData() const;
+
+            /**
+             * Get packet size.
+             *
+             * @return Packet size.
+             */
+            int32_t GetSize() const;
+
+            /**
+             * Check whether data buffer was fully consumed.
+             *
+             * @return @c true if consumed and @c false otherwise.
+             */
+            bool IsEmpty() const;
+
+            /**
+             * Consume the whole buffer.
+             *
+             * @return Buffer containing consumed data.
+             */
+            DataBuffer ConsumeEntirely();
+
+            /**
+             * Get input stream for a data buffer.
+             * @return Stream set up to read data from buffer.
+             */
+            impl::interop::InteropInputStream GetInputStream() const;
+
+            /**
+             * Clone underlying buffer into a new one.
+             *
+             * @return New data buffer.
+             */
+            DataBuffer Clone() const;
+
+            /**
+             * Skip specified number of bytes.
+             *
+             * @param bytes Bytes to skip.
+             */
+            void Skip(int32_t bytes);
+
+        private:
+            /**
+             * Advance position in packet by specified value.
+             *
+             * @param val Bytes to advance.
+             */
+            void Advance(int32_t val);
+
+            /** Position in current data. */
+            int32_t position;
+
+            /** Data length. */
+            int32_t length;
+
+            /** Data */
+            impl::interop::SP_ConstInteropMemory data;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_DATA_BUFFER
diff --git a/modules/platforms/cpp/network/include/ignite/network/data_filter.h b/modules/platforms/cpp/network/include/ignite/network/data_filter.h
new file mode 100644
index 0000000..cb16fc9
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/data_filter.h
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_DATA_FILTER
+#define _IGNITE_NETWORK_DATA_FILTER
+
+#include <ignite/network/data_sink.h>
+#include <ignite/network/async_handler.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Data buffer.
+         */
+        class IGNITE_IMPORT_EXPORT DataFilter : public DataSink, public AsyncHandler
+        {
+        public:
+            /**
+             * Default constructor.
+             */
+            DataFilter() :
+                sink(0),
+                handler(0)
+            {
+                // No-op.
+            }
+
+            /**
+             * Destructor.
+             */
+            virtual ~DataFilter()
+            {
+                // No-op.
+            }
+
+            /**
+             * Set sink.
+             *
+             * @param sink Data sink
+             */
+            void SetSink(DataSink* sink)
+            {
+                this->sink = sink;
+            }
+
+            /**
+             * Get sink.
+             *
+             * @return Data sink.
+             */
+            DataSink* GetSink()
+            {
+                return sink;
+            }
+
+            /**
+             * Set handler.
+             *
+             * @param handler Event handler.
+             */
+            void SetHandler(AsyncHandler* handler)
+            {
+                this->handler = handler;
+            }
+
+            /**
+             * Get handler.
+             *
+             * @return Event handler.
+             */
+            AsyncHandler* GetHandler()
+            {
+                return handler;
+            }
+
+        protected:
+            /** Sink. */
+            DataSink* sink;
+
+            /** Handler. */
+            AsyncHandler* handler;
+        };
+
+        // Shared pointer type alias.
+        typedef common::concurrent::SharedPointer<DataFilter> SP_DataFilter;
+    }
+}
+
+#endif //_IGNITE_NETWORK_DATA_FILTER
diff --git a/modules/platforms/cpp/network/include/ignite/network/data_filter_adapter.h b/modules/platforms/cpp/network/include/ignite/network/data_filter_adapter.h
new file mode 100644
index 0000000..2b2d0c8
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/data_filter_adapter.h
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_DATA_FILTER_ADAPTER
+#define _IGNITE_NETWORK_DATA_FILTER_ADAPTER
+
+#include <ignite/network/data_filter.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Data buffer.
+         */
+        class IGNITE_IMPORT_EXPORT DataFilterAdapter : public DataFilter
+        {
+        public:
+            /**
+             * Default constructor.
+             */
+            DataFilterAdapter()
+            {
+                // No-op.
+            }
+
+            /**
+             * Destructor.
+             */
+            virtual ~DataFilterAdapter()
+            {
+                // No-op.
+            }
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data)
+            {
+                DataSink* sink0 = sink;
+                if (sink0)
+                    return sink0->Send(id, data);
+
+                return false;
+            }
+
+            /**
+             * Closes specified connection if it's established. Connection to the specified address is planned for
+             * re-connect. Error is reported to handler.
+             *
+             * @param id Client ID.
+             */
+            virtual void Close(uint64_t id, const IgniteError* err)
+            {
+                DataSink* sink0 = sink;
+                if (sink0)
+                    sink0->Close(id, err);
+            }
+
+            /**
+              * Callback that called on successful connection establishment.
+              *
+              * @param addr Address of the new connection.
+              * @param id Connection ID.
+              */
+            virtual void OnConnectionSuccess(const EndPoint& addr, uint64_t id)
+            {
+                AsyncHandler* handler0 = handler;
+                if (handler0)
+                    handler0->OnConnectionSuccess(addr, id);
+            }
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param addr Connection address.
+             * @param err Error.
+             */
+            virtual void OnConnectionError(const EndPoint& addr, const IgniteError& err)
+            {
+                AsyncHandler* handler0 = handler;
+                if (handler0)
+                    handler0->OnConnectionError(addr, err);
+            }
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            virtual void OnConnectionClosed(uint64_t id, const IgniteError* err)
+            {
+                AsyncHandler* handler0 = handler;
+                if (handler0)
+                    handler0->OnConnectionClosed(id, err);
+            }
+
+            /**
+             * Callback that called when new message is received.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            virtual void OnMessageReceived(uint64_t id, const DataBuffer& msg)
+            {
+                AsyncHandler* handler0 = handler;
+                if (handler0)
+                    handler0->OnMessageReceived(id, msg);
+            }
+
+            /**
+             * Callback that called when message is sent.
+             *
+             * @param id Async client ID.
+             */
+            virtual void OnMessageSent(uint64_t id)
+            {
+                AsyncHandler* handler0 = handler;
+                if (handler0)
+                    handler0->OnMessageSent(id);
+            }
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_DATA_FILTER_ADAPTER
diff --git a/modules/platforms/cpp/network/include/ignite/network/data_sink.h b/modules/platforms/cpp/network/include/ignite/network/data_sink.h
new file mode 100644
index 0000000..76ab774
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/data_sink.h
@@ -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.
+ */
+
+#ifndef _IGNITE_NETWORK_DATA_SINK
+#define _IGNITE_NETWORK_DATA_SINK
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/network/data_buffer.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Data sink. Can consume data.
+         */
+        class IGNITE_IMPORT_EXPORT DataSink
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~DataSink()
+            {
+                // No-op.
+            }
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data) = 0;
+
+            /**
+             * Closes specified connection if it's established. Connection to the specified address is planned for
+             * re-connect. Error is reported to handler.
+             *
+             * @param id Client ID.
+             */
+            virtual void Close(uint64_t id, const IgniteError* err) = 0;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_DATA_SINK
diff --git a/modules/platforms/cpp/network/include/ignite/network/end_point.h b/modules/platforms/cpp/network/include/ignite/network/end_point.h
index 499cbeb..a1f4945 100644
--- a/modules/platforms/cpp/network/include/ignite/network/end_point.h
+++ b/modules/platforms/cpp/network/include/ignite/network/end_point.h
@@ -20,6 +20,8 @@
 
 #include <stdint.h>
 #include <string>
+#include <sstream>
+#include <vector>
 
 namespace ignite
 {
@@ -53,6 +55,19 @@
             }
 
             /**
+             * Convert to string.
+             *
+             * @return String form.
+             */
+            std::string ToString() const
+            {
+                std::stringstream ss;
+                ss << host << ':' << port;
+
+                return ss.str();
+            }
+
+            /**
              * Compare to another instance.
              *
              * @param other Another instance.
diff --git a/modules/platforms/cpp/network/include/ignite/network/length_prefix_codec.h b/modules/platforms/cpp/network/include/ignite/network/length_prefix_codec.h
new file mode 100644
index 0000000..e11d367
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/length_prefix_codec.h
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_LENGTH_PREFIX_CODEC
+#define _IGNITE_NETWORK_LENGTH_PREFIX_CODEC
+
+#include <ignite/ignite_error.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/codec.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Codec that decodes messages prefixed with int32 length.
+         */
+        class IGNITE_IMPORT_EXPORT LengthPrefixCodec : public Codec
+        {
+        public:
+            enum
+            {
+                /** Packet length header size. */
+                PACKET_HEADER_SIZE = 4
+            };
+
+            /**
+             * Constructor.
+             */
+            LengthPrefixCodec();
+
+            /**
+             * Destructor.
+             */
+            virtual ~LengthPrefixCodec();
+
+            /**
+             * Encode provided data.
+             *
+             * @param data Data to encode.
+             * @return Encoded data. Returning null is ok.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual DataBuffer Encode(DataBuffer& data);
+
+            /**
+             * Decode provided data.
+             *
+             * @param data Data to decode.
+             * @return Decoded data. Returning null means data is not yet ready.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual DataBuffer Decode(DataBuffer& data);
+
+        private:
+            /**
+             * Consume the right amount of provided data to make packet closer to desired size.
+             *
+             * @param data Data to consume.
+             * @param desired Desired resulting size of packet.
+             */
+            void Consume(DataBuffer& data, int32_t desired);
+
+            /** Size of the current packet. */
+            int32_t packetSize;
+
+            /** Current packet */
+            impl::interop::SP_InteropMemory packet;
+        };
+
+        /**
+         * Factory for LengthPrefixCodec.
+         */
+        class IGNITE_IMPORT_EXPORT LengthPrefixCodecFactory : public CodecFactory
+        {
+        public:
+            /**
+             * Constructor.
+             */
+            LengthPrefixCodecFactory()
+            {
+                // No-op.
+            }
+
+            /**
+             * Build instance.
+             *
+             * @return New instance of type @c T.
+             */
+            virtual SP_Codec Build()
+            {
+                return SP_Codec(new LengthPrefixCodec());
+            }
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_LENGTH_PREFIX_CODEC
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/include/ignite/network/network.h b/modules/platforms/cpp/network/include/ignite/network/network.h
index 5e83cc1..850b82a 100644
--- a/modules/platforms/cpp/network/include/ignite/network/network.h
+++ b/modules/platforms/cpp/network/include/ignite/network/network.h
@@ -22,6 +22,8 @@
 
 #include <ignite/common/common.h>
 #include <ignite/network/socket_client.h>
+#include <ignite/network/async_client_pool.h>
+#include <ignite/network/ssl/secure_configuration.h>
 
 namespace ignite
 {
@@ -40,23 +42,29 @@
             IGNITE_IMPORT_EXPORT void EnsureSslLoaded();
 
             /**
-             * Make basic TCP socket.
-             */
-            IGNITE_IMPORT_EXPORT SocketClient* MakeTcpSocketClient();
-
-            /**
              * Make secure socket for SSL/TLS connection.
              *
-             * @param certPath Certificate file path.
-             * @param keyPath Private key file path.
-             * @param caPath Certificate authority file path.
+             * @param cfg Configuration.
              *
              * @throw IgniteError if it is not possible to load SSL library.
              */
-            IGNITE_IMPORT_EXPORT SocketClient* MakeSecureSocketClient(const std::string& certPath,
-                const std::string& keyPath, const std::string& caPath);
+            IGNITE_IMPORT_EXPORT SocketClient* MakeSecureSocketClient(const SecureConfiguration& cfg);
         }
+
+        /**
+         * Make basic TCP socket.
+         */
+        IGNITE_IMPORT_EXPORT SocketClient* MakeTcpSocketClient();
+
+        /**
+         * Make asynchronous client pool.
+         *
+         * @param handler Event handler.
+         * @param filters Filters.
+         * @return Async client pool.
+         */
+        IGNITE_IMPORT_EXPORT SP_AsyncClientPool MakeAsyncClientPool(const std::vector<SP_DataFilter>& filters);
     }
 }
 
-#endif //_IGNITE_NETWORK_SSL_SSL_API
\ No newline at end of file
+#endif //_IGNITE_NETWORK_SSL_SSL_API
diff --git a/modules/platforms/cpp/network/include/ignite/network/socket_client.h b/modules/platforms/cpp/network/include/ignite/network/socket_client.h
index eb1d481..2ad8d0a 100644
--- a/modules/platforms/cpp/network/include/ignite/network/socket_client.h
+++ b/modules/platforms/cpp/network/include/ignite/network/socket_client.h
@@ -18,10 +18,9 @@
 #ifndef _IGNITE_NETWORK_SOCKET_CLIENT
 #define _IGNITE_NETWORK_SOCKET_CLIENT
 
+#include <stddef.h>
 #include <stdint.h>
 
-#include <ignite/ignite_error.h>
-
 namespace ignite
 {
     namespace network
@@ -97,19 +96,8 @@
              * @return @c true if the socket is blocking and false otherwise.
              */
             virtual bool IsBlocking() const = 0;
-
-        protected:
-            /**
-             * Throw connection error.
-             *
-             * @param err Error message.
-             */
-            static void ThrowNetworkError(const std::string& err)
-            {
-                throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, err.c_str());
-            }
         };
     }
 }
 
-#endif //_IGNITE_NETWORK_SOCKET_CLIENT
\ No newline at end of file
+#endif //_IGNITE_NETWORK_SOCKET_CLIENT
diff --git a/modules/platforms/cpp/network/include/ignite/network/ssl/secure_configuration.h b/modules/platforms/cpp/network/include/ignite/network/ssl/secure_configuration.h
new file mode 100644
index 0000000..0f08d94
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/ssl/secure_configuration.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_SSL_SECURE_CONFIGURATION
+#define _IGNITE_NETWORK_SSL_SECURE_CONFIGURATION
+
+#include <string>
+
+namespace ignite
+{
+    namespace network
+    {
+        namespace ssl
+        {
+            /**
+             * TLS/SSL configuration parameters.
+             */
+            struct SecureConfiguration
+            {
+                /** Path to file containing security certificate to use. */
+                std::string certPath;
+
+                /** Path to file containing private key to use. */
+                std::string keyPath;
+
+                /** Path to file containing Certificate authority to use. */
+                std::string caPath;
+            };
+        }
+    }
+}
+
+#endif //_IGNITE_NETWORK_SSL_SECURE_CONFIGURATION
diff --git a/modules/platforms/cpp/network/include/ignite/network/ssl/secure_data_filter.h b/modules/platforms/cpp/network/include/ignite/network/ssl/secure_data_filter.h
new file mode 100644
index 0000000..175705b
--- /dev/null
+++ b/modules/platforms/cpp/network/include/ignite/network/ssl/secure_data_filter.h
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_SSL_SECURE_DATA_FILTER
+#define _IGNITE_NETWORK_SSL_SECURE_DATA_FILTER
+
+#include <map>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/network/data_filter_adapter.h>
+#include <ignite/network/ssl/secure_configuration.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        namespace ssl
+        {
+            /**
+             * TLS/SSL Data Filter.
+             */
+            class IGNITE_IMPORT_EXPORT SecureDataFilter : public DataFilterAdapter
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param cfg Configuration.
+                 */
+                explicit SecureDataFilter(const SecureConfiguration& cfg);
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~SecureDataFilter();
+
+                /**
+                 * Send data to specific established connection.
+                 *
+                 * @param id Client ID.
+                 * @param data Data to be sent.
+                 * @return @c true if connection is present and @c false otherwise.
+                 *
+                 * @throw IgniteError on error.
+                 */
+                virtual bool Send(uint64_t id, const DataBuffer& data);
+
+                /**
+                  * Callback that called on successful connection establishment.
+                  *
+                  * @param addr Address of the new connection.
+                  * @param id Connection ID.
+                  */
+                virtual void OnConnectionSuccess(const EndPoint& addr, uint64_t id);
+
+                /**
+                 * Callback that called on error during connection establishment.
+                 *
+                 * @param id Async client ID.
+                 * @param err Error. Can be null if connection closed without error.
+                 */
+                virtual void OnConnectionClosed(uint64_t id, const IgniteError* err);
+
+                /**
+                 * Callback that called when new message is received.
+                 *
+                 * @param id Async client ID.
+                 * @param msg Received message.
+                 */
+                virtual void OnMessageReceived(uint64_t id, const DataBuffer& msg);
+
+            private:
+                IGNITE_NO_COPY_ASSIGNMENT(SecureDataFilter);
+
+                /**
+                 * Secure connection context.
+                 */
+                class SecureConnectionContext
+                {
+                public:
+                    /**
+                     * Default constructor.
+                     *
+                     * @param id Connection ID.
+                     * @param addr Address.
+                     * @param filter Filter.
+                     */
+                    SecureConnectionContext(uint64_t id, const EndPoint &addr, SecureDataFilter& filter);
+
+                    /**
+                     * Destructor.
+                     */
+                    ~SecureConnectionContext();
+
+                    /**
+                     * Start connection procedure including handshake.
+                     *
+                     * @return @c true, if connection complete.
+                     */
+                    bool DoConnect();
+
+                    /**
+                     * Check whether connection is established.
+                     *
+                     * @return @c true if connection established.
+                     */
+                    bool IsConnected() const
+                    {
+                        return connected;
+                    }
+
+                    /**
+                     * Get address.
+                     *
+                     * @return Address.
+                     */
+                    const EndPoint& GetAddress() const
+                    {
+                        return addr;
+                    }
+
+                    /**
+                     * Send data.
+                     *
+                     * @param data Data to send.
+                     * @return @c true on success.
+                     */
+                    bool Send(const DataBuffer& data);
+
+                    /**
+                     * Process new data.
+                     *
+                     * @param data Data received.
+                     * @return @c true if connection was established.
+                     */
+                    bool ProcessData(DataBuffer& data);
+
+                    /**
+                     * Get pending decrypted data.
+                     *
+                     * @return Data buffer.
+                     */
+                    DataBuffer GetPendingDecryptedData();
+
+                private:
+                    enum
+                    {
+                        /** Receive buffer size. */
+                        RECEIVE_BUFFER_SIZE = 0x10000
+                    };
+
+                    /**
+                     * Send pending data.
+                     */
+                    bool SendPendingData();
+
+                    /**
+                     * Get pending data.
+                     *
+                     * @param bio BIO to get data from.
+                     * @return Data buffer.
+                     */
+                    static DataBuffer GetPendingData(void* bio);
+
+                    /** Flag indicating that secure connection is established. */
+                    bool connected;
+
+                    /** Connection ID. */
+                    const uint64_t id;
+
+                    /** Address. */
+                    const EndPoint addr;
+
+                    /** Filter. */
+                    SecureDataFilter& filter;
+
+                    /** Receive buffer. */
+                    impl::interop::SP_InteropMemory recvBuffer;
+
+                    /** SSL instance. */
+                    void* ssl;
+
+                    /** Input BIO. */
+                    void* bioIn;
+
+                    /** Output BIO. */
+                    void* bioOut;
+                };
+
+                // Shared pointer type alias.
+                typedef common::concurrent::SharedPointer<SecureConnectionContext> SP_SecureConnectionContext;
+
+                /** Context map. */
+                typedef std::map<uint64_t, SP_SecureConnectionContext> ContextMap;
+
+                /**
+                 * Get context for connection.
+                 *
+                 * @param id Connection ID.
+                 * @return Context if found or null.
+                 */
+                SP_SecureConnectionContext FindContext(uint64_t id);
+
+                /**
+                 * Send data to specific established connection.
+                 *
+                 * @param id Client ID.
+                 * @param data Data to be sent.
+                 * @return @c true if connection is present and @c false otherwise.
+                 *
+                 * @throw IgniteError on error.
+                 */
+                bool SendInternal(uint64_t id, const DataBuffer& data);
+
+                /** SSL context. */
+                void* sslContext;
+
+                /** Contexts for connections. */
+                ContextMap* contexts;
+
+                /** Mutex for secure access to context map. */
+                common::concurrent::CriticalSection contextCs;
+            };
+
+            // Shared pointer type alias.
+            typedef common::concurrent::SharedPointer<SecureDataFilter> SP_SecureDataFilter;
+        }
+    }
+}
+
+#endif //_IGNITE_NETWORK_SSL_SECURE_DATA_FILTER
diff --git a/modules/platforms/cpp/network/include/ignite/network/tcp_range.h b/modules/platforms/cpp/network/include/ignite/network/tcp_range.h
index 8644d46..a64df64 100644
--- a/modules/platforms/cpp/network/include/ignite/network/tcp_range.h
+++ b/modules/platforms/cpp/network/include/ignite/network/tcp_range.h
@@ -20,6 +20,7 @@
 
 #include <stdint.h>
 #include <string>
+#include <sstream>
 
 namespace ignite
 {
@@ -81,6 +82,16 @@
             }
 
             /**
+             * Check whether empty.
+             *
+             * @return @c true if empty.
+             */
+            bool IsEmpty() const
+            {
+                return host.empty();
+            }
+
+            /**
              * Comparison operator.
              *
              * @param val1 First value.
@@ -153,19 +164,32 @@
                 return val1.Compare(val2) >= 0;
             }
 
+            /**
+             * Convert to string.
+             *
+             * @return String representation.
+             */
+            std::string ToString() const
+            {
+                std::stringstream buf;
+                buf << host << ':' << port;
+
+                if (range)
+                    buf << ".." << (port + range);
+
+                return buf.str();
+            }
+
             /** Remote host. */
             std::string host;
 
             /** TCP port. */
             uint16_t port;
 
-            /**
-             * Number of ports after the port that should be tried if
-             * the previous are unavailable.
-             */
+            /** Number of ports after the port that should be tried if the previous are unavailable. */
             uint16_t range;
         };
     }
 }
 
-#endif //_IGNITE_NETWORK_TCP_RANGE
\ No newline at end of file
+#endif //_IGNITE_NETWORK_TCP_RANGE
diff --git a/modules/platforms/cpp/network/include/ignite/network/utils.h b/modules/platforms/cpp/network/include/ignite/network/utils.h
index e8f0851..082ac97 100644
--- a/modules/platforms/cpp/network/include/ignite/network/utils.h
+++ b/modules/platforms/cpp/network/include/ignite/network/utils.h
@@ -22,6 +22,7 @@
 #include <string>
 
 #include <ignite/common/common.h>
+#include <ignite/ignite_error.h>
 
 namespace ignite
 {
@@ -35,6 +36,16 @@
              * @param addrs Addresses set.
              */
             void IGNITE_IMPORT_EXPORT GetLocalAddresses(std::set<std::string>& addrs);
+
+            /**
+             * Throw connection error.
+             *
+             * @param err Error message.
+             */
+            inline void ThrowNetworkError(const std::string& err)
+            {
+                throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, err.c_str());
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/network/os/linux/src/network/connecting_context.cpp b/modules/platforms/cpp/network/os/linux/src/network/connecting_context.cpp
new file mode 100644
index 0000000..4e528ff
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/connecting_context.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netinet/tcp.h>
+#include <netdb.h>
+#include <sys/epoll.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <iterator>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/connecting_context.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        ConnectingContext::ConnectingContext(const TcpRange &range) :
+            range(range),
+            nextPort(range.port),
+            info(0),
+            currentInfo(0)
+        {
+            // No-op.
+        }
+
+        ConnectingContext::~ConnectingContext()
+        {
+            Reset();
+        }
+
+        void ConnectingContext::Reset()
+        {
+            if (info)
+            {
+                freeaddrinfo(info);
+                info = 0;
+                currentInfo = 0;
+            }
+
+            nextPort = range.port;
+        }
+
+        addrinfo *ConnectingContext::Next()
+        {
+            if (currentInfo)
+                currentInfo = currentInfo->ai_next;
+
+            while (!currentInfo)
+            {
+                if (info)
+                {
+                    freeaddrinfo(info);
+                    info = 0;
+                }
+
+                if (nextPort > range.port + range.range)
+                    return 0;
+
+                addrinfo hints;
+                std::memset(&hints, 0, sizeof(hints));
+
+                hints.ai_family = AF_UNSPEC;
+                hints.ai_socktype = SOCK_STREAM;
+                hints.ai_protocol = IPPROTO_TCP;
+
+                std::string strPort = common::LexicalCast<std::string>(nextPort);
+
+                // Resolve the server address and port
+                int res = getaddrinfo(range.host.c_str(), strPort.c_str(), &hints, &info);
+                if (res != 0)
+                    return 0;
+
+                currentInfo = info;
+                ++nextPort;
+            }
+
+            return currentInfo;
+        }
+
+        EndPoint ConnectingContext::GetAddress() const
+        {
+            return EndPoint(range.host, nextPort - 1);
+        }
+
+        SP_LinuxAsyncClient ConnectingContext::ToClient(int fd)
+        {
+            return SP_LinuxAsyncClient(new LinuxAsyncClient(fd, GetAddress(), range));
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/linux/src/network/connecting_context.h b/modules/platforms/cpp/network/os/linux/src/network/connecting_context.h
new file mode 100644
index 0000000..ee10286
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/connecting_context.h
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_CONNECTING_CONTEXT
+#define _IGNITE_NETWORK_CONNECTING_CONTEXT
+
+#include <netdb.h>
+
+#include <stdint.h>
+#include <memory>
+
+#include <ignite/network/end_point.h>
+#include <ignite/network/tcp_range.h>
+
+#include "network/linux_async_client.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Connecting context.
+         */
+        class ConnectingContext
+        {
+        public:
+            /**
+             * Constructor.
+             */
+            ConnectingContext(const TcpRange& range);
+
+            /**
+             * Destructor.
+             */
+            ~ConnectingContext();
+
+            /**
+             * Reset connection context to it's initial state.
+             */
+            void Reset();
+
+            /**
+             * Next address in range.
+             *
+             * @return Next addrinfo for connection.
+             */
+            addrinfo* Next();
+
+            /**
+             * Get lastaddress.
+             *
+             * @return Address.
+             */
+            EndPoint GetAddress() const;
+
+            /**
+             * Make client.
+             *
+             * @param fd Socket file descriptor.
+             * @return Client instance from current internal state.
+             */
+            SP_LinuxAsyncClient ToClient(int fd);
+
+        private:
+            IGNITE_NO_COPY_ASSIGNMENT(ConnectingContext);
+
+            /** Range. */
+            const TcpRange range;
+
+            /** Next port. */
+            uint16_t nextPort;
+
+            /** Current addrinfo. */
+            addrinfo* info;
+
+            /** Addrinfo which is currently used for connection */
+            addrinfo* currentInfo;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_CONNECTING_CONTEXT
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.cpp b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.cpp
new file mode 100644
index 0000000..a1431c6
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netinet/tcp.h>
+#include <netdb.h>
+#include <sys/epoll.h>
+#include <unistd.h>
+
+#include <algorithm>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/linux_async_client.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        LinuxAsyncClient::LinuxAsyncClient(int fd, const EndPoint &addr, const TcpRange &range) :
+            state(State::CONNECTED),
+            fd(fd),
+            epoll(-1),
+            id(0),
+            addr(addr),
+            range(range),
+            sendPackets(),
+            sendCs(),
+            recvPacket(),
+            closeErr(IgniteError::IGNITE_SUCCESS)
+        {
+            // No-op.
+        }
+
+        LinuxAsyncClient::~LinuxAsyncClient()
+        {
+            Shutdown(0);
+
+            Close();
+        }
+
+        bool LinuxAsyncClient::Shutdown(const IgniteError* err)
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+            if (state != State::CONNECTED)
+                return false;
+
+            closeErr = err ? *err : IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Connection closed by application");
+            shutdown(fd, SHUT_RDWR);
+            state = State::SHUTDOWN;
+
+            return true;
+        }
+
+        bool LinuxAsyncClient::Close()
+        {
+            if (State::CLOSED == state)
+                return false;
+
+            StopMonitoring();
+            close(fd);
+            fd = -1;
+            state = State::CLOSED;
+
+            return true;
+        }
+
+        bool LinuxAsyncClient::Send(const DataBuffer& data)
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+
+            sendPackets.push_back(data);
+
+            if (sendPackets.size() > 1)
+                return true;
+
+            return SendNextPacketLocked();
+        }
+
+        bool LinuxAsyncClient::SendNextPacketLocked()
+        {
+            if (sendPackets.empty())
+                return true;
+
+            DataBuffer& packet = sendPackets.front();
+
+            ssize_t ret = send(fd, packet.GetData(), packet.GetSize(), 0);
+            if (ret < 0)
+                return false;
+
+            packet.Skip(static_cast<int32_t>(ret));
+
+            EnableSendNotifications();
+
+            return true;
+        }
+
+        DataBuffer LinuxAsyncClient::Receive()
+        {
+            using namespace impl::interop;
+
+            if (!recvPacket.IsValid())
+            {
+                recvPacket = SP_InteropMemory(new InteropUnpooledMemory(BUFFER_SIZE));
+                recvPacket.Get()->Length(BUFFER_SIZE);
+            }
+
+            ssize_t res = recv(fd, recvPacket.Get()->Data(), recvPacket.Get()->Length(), 0);
+            if (res < 0)
+                return DataBuffer();
+
+            return DataBuffer(recvPacket, 0, static_cast<int32_t>(res));
+        }
+
+        bool LinuxAsyncClient::StartMonitoring(int epoll0)
+        {
+            if (epoll0 < 0)
+                return false;
+
+            epoll_event event;
+            std::memset(&event, 0, sizeof(event));
+            event.data.ptr = this;
+            event.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP;
+
+            int res = epoll_ctl(epoll0, EPOLL_CTL_ADD, fd, &event);
+            if (res < 0)
+                return false;
+
+            epoll = epoll0;
+
+            return true;
+        }
+
+        void LinuxAsyncClient::StopMonitoring()
+        {
+            epoll_event event;
+            std::memset(&event, 0, sizeof(event));
+
+            epoll_ctl(epoll, EPOLL_CTL_DEL, fd, &event);
+        }
+
+        void LinuxAsyncClient::EnableSendNotifications()
+        {
+            epoll_event event;
+            std::memset(&event, 0, sizeof(event));
+            event.data.ptr = this;
+            event.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP;
+
+            epoll_ctl(epoll, EPOLL_CTL_MOD, fd, &event);
+        }
+
+        void LinuxAsyncClient::DisableSendNotifications()
+        {
+            epoll_event event;
+            std::memset(&event, 0, sizeof(event));
+            event.data.ptr = this;
+            event.events = EPOLLIN | EPOLLRDHUP;
+
+            epoll_ctl(epoll, EPOLL_CTL_MOD, fd, &event);
+        }
+
+        bool LinuxAsyncClient::ProcessSent()
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+
+            if (sendPackets.empty())
+            {
+                DisableSendNotifications();
+
+                return true;
+            }
+
+            DataBuffer& front = sendPackets.front();
+
+            if (front.IsEmpty())
+                sendPackets.pop_front();
+
+            return SendNextPacketLocked();
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.h b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.h
new file mode 100644
index 0000000..79977fc
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client.h
@@ -0,0 +1,249 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_LINUX_ASYNC_CLIENT
+#define _IGNITE_NETWORK_LINUX_ASYNC_CLIENT
+
+#include "network/sockets.h"
+
+#include <stdint.h>
+#include <deque>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_handler.h>
+#include <ignite/network/codec.h>
+#include <ignite/network/end_point.h>
+#include <ignite/network/tcp_range.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Linux-specific implementation of async network client.
+         */
+        class LinuxAsyncClient
+        {
+            /**
+             * State.
+             */
+            struct State
+            {
+                enum Type
+                {
+                    CONNECTED,
+
+                    SHUTDOWN,
+
+                    CLOSED,
+                };
+            };
+
+        public:
+            enum { BUFFER_SIZE = 0x10000 };
+
+            /**
+             * Constructor.
+             *
+             * @param fd Socket file descriptor.
+             * @param addr Address.
+             * @param range Range.
+             */
+            LinuxAsyncClient(int fd, const EndPoint& addr, const TcpRange& range);
+
+            /**
+             * Destructor.
+             *
+             * Should not be destructed from external threads.
+             * Can be destructed from WorkerThread.
+             */
+            ~LinuxAsyncClient();
+
+            /**
+             * Shutdown client.
+             *
+             * Can be called from external threads.
+             * Can be called from WorkerThread.
+             *
+             * @param err Error message. Can be null.
+             * @return @c true if shutdown performed successfully.
+             */
+            bool Shutdown(const IgniteError* err);
+
+            /**
+             * Close client.
+             *
+             * Should not be called from external threads.
+             * Can be called from WorkerThread.
+             *
+             * @return @c true if shutdown performed successfully.
+             */
+            bool Close();
+
+            /**
+             * Send packet using client.
+             *
+             * @param data Data to send.
+             * @return @c true on success.
+             */
+            bool Send(const DataBuffer& data);
+
+            /**
+             * Initiate next receive of data.
+             *
+             * @return @c true on success.
+             */
+            DataBuffer Receive();
+
+            /**
+             * Process sent data.
+             *
+             * @return @c true on success.
+             */
+            bool ProcessSent();
+
+            /**
+             * Start monitoring client.
+             *
+             * @param epoll Epoll file descriptor.
+             * @return @c true on success.
+             */
+            bool StartMonitoring(int epoll);
+
+            /**
+             * Stop monitoring client.
+             */
+            void StopMonitoring();
+
+            /**
+             * Enable epoll notifications.
+             */
+            void EnableSendNotifications();
+
+            /**
+             * Disable epoll notifications.
+             */
+            void DisableSendNotifications();
+
+            /**
+             * Get client ID.
+             *
+             * @return Client ID.
+             */
+            uint64_t GetId() const
+            {
+                return id;
+            }
+
+            /**
+             * Set ID.
+             *
+             * @param id ID to set.
+             */
+            void SetId(uint64_t id)
+            {
+                this->id = id;
+            }
+
+            /**
+             * Get address.
+             *
+             * @return Address.
+             */
+            const EndPoint& GetAddress() const
+            {
+                return addr;
+            }
+
+            /**
+             * Get range.
+             *
+             * @return Range.
+             */
+            const TcpRange& GetRange() const
+            {
+                return range;
+            }
+
+            /**
+             * Check whether client is closed.
+             *
+             * @return @c true if closed.
+             */
+            bool IsClosed() const
+            {
+                return state == State::CLOSED;
+            }
+
+            /**
+             * Get closing error for the connection. Can be IGNITE_SUCCESS.
+             *
+             * @return Connection error.
+             */
+            const IgniteError& GetCloseError() const
+            {
+                return closeErr;
+            }
+
+        private:
+            /**
+             * Send next packet in queue.
+             *
+             * @warning Can only be called when holding sendCs lock.
+             * @return @c true on success.
+             */
+            bool SendNextPacketLocked();
+
+            /** State. */
+            State::Type state;
+
+            /** Socket file descriptor. */
+            int fd;
+
+            /** Epoll file descriptor. */
+            int epoll;
+
+            /** Connection ID. */
+            uint64_t id;
+
+            /** Server end point. */
+            EndPoint addr;
+
+            /** Address range associated with current connection. */
+            TcpRange range;
+
+            /** Packets that should be sent. */
+            std::deque<DataBuffer> sendPackets;
+
+            /** Send critical section. */
+            common::concurrent::CriticalSection sendCs;
+
+            /** Packet that is currently received. */
+            impl::interop::SP_InteropMemory recvPacket;
+
+            /** Closing error. */
+            IgniteError closeErr;
+        };
+
+        /** Shared pointer to async client. */
+        typedef common::concurrent::SharedPointer<LinuxAsyncClient> SP_LinuxAsyncClient;
+    }
+}
+
+#endif //_IGNITE_NETWORK_LINUX_ASYNC_CLIENT
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.cpp b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.cpp
new file mode 100644
index 0000000..286183a
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.cpp
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/linux_async_client_pool.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        LinuxAsyncClientPool::LinuxAsyncClientPool() :
+            stopping(true),
+            asyncHandler(0),
+            workerThread(*this),
+            idGen(0),
+            clientsCs(),
+            clientIdMap()
+        {
+            // No-op.
+        }
+
+        LinuxAsyncClientPool::~LinuxAsyncClientPool()
+        {
+            InternalStop();
+        }
+
+        void LinuxAsyncClientPool::Start(const std::vector<TcpRange> &addrs, uint32_t connLimit)
+        {
+            if (!stopping)
+                throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Client pool is already started");
+
+            idGen = 0;
+            stopping = false;
+
+            try
+            {
+                workerThread.Start0(connLimit, addrs);
+            }
+            catch (...)
+            {
+                Stop();
+
+                throw;
+            }
+        }
+
+        void LinuxAsyncClientPool::Stop()
+        {
+            InternalStop();
+        }
+
+        bool LinuxAsyncClientPool::Send(uint64_t id, const DataBuffer &data)
+        {
+            if (stopping)
+                return false;
+
+            SP_LinuxAsyncClient client = FindClient(id);
+            if (!client.IsValid())
+                return false;
+
+            return client.Get()->Send(data);
+        }
+
+        void LinuxAsyncClientPool::Close(uint64_t id, const IgniteError *err)
+        {
+            if (stopping)
+                return;
+
+            SP_LinuxAsyncClient client = FindClient(id);
+            if (client.IsValid() && !client.Get()->IsClosed())
+                client.Get()->Shutdown(err);
+        }
+
+        void LinuxAsyncClientPool::CloseAndRelease(uint64_t id, const IgniteError *err)
+        {
+            if (stopping)
+                return;
+
+            SP_LinuxAsyncClient client;
+            {
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                std::map<uint64_t, SP_LinuxAsyncClient>::iterator it = clientIdMap.find(id);
+                if (it == clientIdMap.end())
+                    return;
+
+                client = it->second;
+
+                clientIdMap.erase(it);
+            }
+
+            bool closed = client.Get()->Close();
+            if (closed)
+            {
+                IgniteError err0(client.Get()->GetCloseError());
+                if (err0.GetCode() == IgniteError::IGNITE_SUCCESS)
+                    err0 = IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, "Connection closed by server");
+
+                if (!err)
+                    err = &err0;
+
+                HandleConnectionClosed(id, err);
+            }
+        }
+
+        bool LinuxAsyncClientPool::AddClient(SP_LinuxAsyncClient &client)
+        {
+            if (stopping)
+                return false;
+
+            LinuxAsyncClient& clientRef = *client.Get();
+            {
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                uint64_t id = ++idGen;
+                clientRef.SetId(id);
+
+                clientIdMap[id] = client;
+            }
+
+            HandleConnectionSuccess(clientRef.GetAddress(), clientRef.GetId());
+
+            return true;
+        }
+
+        void LinuxAsyncClientPool::HandleConnectionError(const EndPoint &addr, const IgniteError &err)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionError(addr, err);
+        }
+
+        void LinuxAsyncClientPool::HandleConnectionSuccess(const EndPoint &addr, uint64_t id)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionSuccess(addr, id);
+        }
+
+        void LinuxAsyncClientPool::HandleConnectionClosed(uint64_t id, const IgniteError *err)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionClosed(id, err);
+        }
+
+        void LinuxAsyncClientPool::HandleMessageReceived(uint64_t id, const DataBuffer &msg)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnMessageReceived(id, msg);
+        }
+
+        void LinuxAsyncClientPool::HandleMessageSent(uint64_t id)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnMessageSent(id);
+        }
+
+        void LinuxAsyncClientPool::InternalStop()
+        {
+            stopping = true;
+            workerThread.Stop();
+
+            {
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                std::map<uint64_t, SP_LinuxAsyncClient>::iterator it;
+                for (it = clientIdMap.begin(); it != clientIdMap.end(); ++it)
+                {
+                    LinuxAsyncClient& client = *it->second.Get();
+
+                    IgniteError err(IgniteError::IGNITE_ERR_GENERIC, "Client stopped");
+                    HandleConnectionClosed(client.GetId(), &err);
+                }
+
+                clientIdMap.clear();
+            }
+        }
+
+        SP_LinuxAsyncClient LinuxAsyncClientPool::FindClient(uint64_t id) const
+        {
+            common::concurrent::CsLockGuard lock(clientsCs);
+
+            std::map<uint64_t, SP_LinuxAsyncClient>::const_iterator it = clientIdMap.find(id);
+            if (it == clientIdMap.end())
+                return SP_LinuxAsyncClient();
+
+            return it->second;
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.h b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.h
new file mode 100644
index 0000000..ca47894
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_client_pool.h
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_LINUX_ASYNC_CLIENT_POOL
+#define _IGNITE_NETWORK_LINUX_ASYNC_CLIENT_POOL
+
+#include <stdint.h>
+#include <map>
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_client_pool.h>
+#include <ignite/network/async_handler.h>
+#include <ignite/network/tcp_range.h>
+
+#include "network/linux_async_worker_thread.h"
+#include "network/linux_async_client.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Linux-specific implementation of asynchronous client pool.
+         */
+        class LinuxAsyncClientPool : public AsyncClientPool
+        {
+        public:
+            /**
+             * Constructor
+             *
+             * @param handler Upper level event handler.
+             */
+            LinuxAsyncClientPool();
+
+            /**
+             * Destructor.
+             */
+            virtual ~LinuxAsyncClientPool();
+
+            /**
+             * Start internal thread that establishes connections to provided addresses and asynchronously sends and
+             * receives messages from them. Function returns either when thread is started and first connection is
+             * established or failure happened.
+             *
+             * @param addrs Addresses to connect to.
+             * @param connLimit Connection upper limit. Zero means limit is disabled.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual void Start(const std::vector<TcpRange>& addrs, uint32_t connLimit);
+
+            /**
+             * Close all established connections and stops handling thread.
+             */
+            virtual void Stop();
+
+            /**
+             * Set handler.
+             *
+             * @param handler Handler to set.
+             */
+            virtual void SetHandler(AsyncHandler *handler)
+            {
+                asyncHandler = handler;
+            }
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data);
+
+            /**
+             * Closes specified connection if it's established. Connection to the specified address is planned for
+             * re-connect. Event is issued to the handler with specified error.
+             *
+             * @param id Client ID.
+             */
+            virtual void Close(uint64_t id, const IgniteError* err);
+
+            /**
+             * Closes and releases memory allocated for client with specified ID.
+             * Error is reported to handler.
+             *
+             * @param id Client ID.
+             * @param err Error to report. May be null.
+             * @return @c true if connection with specified ID was found.
+             */
+            void CloseAndRelease(uint64_t id, const IgniteError* err);
+
+            /**
+             * Add client to connection map. Notify user.
+             *
+             * @param client Client.
+             * @return Client ID.
+             */
+            bool AddClient(SP_LinuxAsyncClient& client);
+
+            /**
+             * Handle error during connection establishment.
+             *
+             * @param addr Connection address.
+             * @param err Error.
+             */
+            void HandleConnectionError(const EndPoint& addr, const IgniteError& err);
+
+            /**
+             * Handle successful connection establishment.
+             *
+             * @param addr Address of the new connection.
+             * @param id Connection ID.
+             */
+            void HandleConnectionSuccess(const EndPoint& addr, uint64_t id);
+
+            /**
+             * Handle error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            void HandleConnectionClosed(uint64_t id, const IgniteError* err);
+
+            /**
+             * Handle new message.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            void HandleMessageReceived(uint64_t id, const DataBuffer& msg);
+
+            /**
+             * Handle sent message event.
+             *
+             * @param id Async client ID.
+             */
+            void HandleMessageSent(uint64_t id);
+
+        private:
+             /**
+             * Close all established connections and stops handling threads.
+             */
+            void InternalStop();
+
+            /**
+             * Find client by ID.
+             *
+             * @param id Client ID.
+             * @return Client. Null pointer if is not found.
+             */
+            SP_LinuxAsyncClient FindClient(uint64_t id) const;
+
+            /**
+             * Find client by ID.
+             *
+             * @warning Should only be called with clientsCs lock held.
+             * @param id Client ID.
+             * @return Client. Null pointer if is not found.
+             */
+            SP_LinuxAsyncClient FindClientLocked(uint64_t id) const;
+
+            /** Flag indicating that pool is stopping. */
+            volatile bool stopping;
+
+            /** Event handler. */
+            AsyncHandler* asyncHandler;
+
+            /** Worker thread. */
+            LinuxAsyncWorkerThread workerThread;
+
+            /** ID counter. */
+            uint64_t idGen;
+
+            /** Clients critical section. */
+            mutable common::concurrent::CriticalSection clientsCs;
+
+            /** Client mapping ID -> client */
+            std::map<uint64_t, SP_LinuxAsyncClient> clientIdMap;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_LINUX_ASYNC_CLIENT_POOL
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.cpp b/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.cpp
new file mode 100644
index 0000000..67529c0
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.cpp
@@ -0,0 +1,348 @@
+/*
+ * 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.
+ */
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/epoll.h>
+#include <sys/eventfd.h>
+#include <netdb.h>
+#include <netinet/tcp.h>
+#include <unistd.h>
+
+#include <cstring>
+
+#include <ignite/network/utils.h>
+#include <ignite/common/utils.h>
+
+#include "network/linux_async_worker_thread.h"
+#include "network/linux_async_client_pool.h"
+
+namespace
+{
+    ignite::common::FibonacciSequence<10> fibonacci10;
+}
+
+namespace ignite
+{
+    namespace network
+    {
+        LinuxAsyncWorkerThread::LinuxAsyncWorkerThread(LinuxAsyncClientPool &clientPool) :
+            clientPool(clientPool),
+            stopping(true),
+            epoll(-1),
+            stopEvent(-1),
+            nonConnected(),
+            currentConnection(),
+            currentClient(),
+            failedAttempts(0),
+            lastConnectionTime(),
+            minAddrs(0)
+        {
+            memset(&lastConnectionTime, 0, sizeof(lastConnectionTime));
+        }
+
+        LinuxAsyncWorkerThread::~LinuxAsyncWorkerThread()
+        {
+            Stop();
+        }
+
+        void LinuxAsyncWorkerThread::Start0(size_t limit, const std::vector<TcpRange> &addrs)
+        {
+            epoll = epoll_create(1);
+            if (epoll < 0)
+                common::ThrowLastSystemError("Failed to create epoll instance");
+
+            stopEvent = eventfd(0, EFD_NONBLOCK);
+            if (stopEvent < 0)
+            {
+                std::string msg = common::GetLastSystemError("Failed to create stop event instance");
+                close(stopEvent);
+                common::ThrowSystemError(msg);
+            }
+
+            epoll_event event;
+            memset(&event, 0, sizeof(event));
+
+            event.events = EPOLLIN;
+
+            int res = epoll_ctl(epoll, EPOLL_CTL_ADD, stopEvent, &event);
+            if (res < 0)
+            {
+                std::string msg = common::GetLastSystemError("Failed to create stop event instance");
+                close(stopEvent);
+                close(epoll);
+                common::ThrowSystemError(msg);
+            }
+
+            stopping = false;
+            failedAttempts = 0;
+            nonConnected = addrs;
+
+            currentConnection.reset();
+            currentClient = SP_LinuxAsyncClient();
+
+            if (!limit || limit > addrs.size())
+                minAddrs = 0;
+            else
+                minAddrs = addrs.size() - limit;
+
+            Thread::Start();
+        }
+
+        void LinuxAsyncWorkerThread::Stop()
+        {
+            if (stopping)
+                return;
+
+            stopping = true;
+
+            int64_t value = 1;
+            ssize_t res = write(stopEvent, &value, sizeof(value));
+
+            IGNITE_UNUSED(res);
+            assert(res == sizeof(value));
+
+            Thread::Join();
+
+            close(stopEvent);
+            close(epoll);
+
+            nonConnected.clear();
+            currentConnection.reset();
+        }
+
+        void LinuxAsyncWorkerThread::Run()
+        {
+            while (!stopping)
+            {
+                HandleNewConnections();
+
+                if (stopping)
+                    break;
+
+                HandleConnectionEvents();
+            }
+        }
+
+        void LinuxAsyncWorkerThread::HandleNewConnections()
+        {
+            if (!ShouldInitiateNewConnection())
+                return;
+
+            if (CalculateConnectionTimeout() > 0)
+                return;
+
+            addrinfo* addr = 0;
+            if (currentConnection.get())
+                addr = currentConnection->Next();
+
+            if (!addr)
+            {
+                size_t idx = rand() % nonConnected.size();
+                const TcpRange& range = nonConnected.at(idx);
+
+                currentConnection.reset(new ConnectingContext(range));
+                addr = currentConnection->Next();
+                if (!addr)
+                {
+                    currentConnection.reset();
+                    ReportConnectionError(EndPoint(), "Can not resolve a single address from range: " + range.ToString());
+                    ++failedAttempts;
+
+                    return;
+                }
+            }
+
+            // Create a SOCKET for connecting to server
+            int socketFd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
+            if (SOCKET_ERROR == socketFd)
+            {
+                ReportConnectionError(currentConnection->GetAddress(),
+                    "Socket creation failed: " + sockets::GetLastSocketErrorMessage());
+
+                return;
+            }
+
+            sockets::TrySetSocketOptions(socketFd, LinuxAsyncClient::BUFFER_SIZE, true, true, true);
+            bool success = sockets::SetNonBlockingMode(socketFd, true);
+            if (!success)
+            {
+                ReportConnectionError(currentConnection->GetAddress(),
+                    "Can not make non-blocking socket: " + sockets::GetLastSocketErrorMessage());
+
+                return;
+            }
+
+            currentClient = currentConnection->ToClient(socketFd);
+            bool ok = currentClient.Get()->StartMonitoring(epoll);
+            if (!ok)
+                common::ThrowLastSystemError("Can not add file descriptor to epoll");
+
+            // Connect to server.
+            int res = connect(socketFd, addr->ai_addr, addr->ai_addrlen);
+            if (SOCKET_ERROR == res)
+            {
+                int lastError = errno;
+
+                clock_gettime(CLOCK_MONOTONIC, &lastConnectionTime);
+
+                if (lastError != EWOULDBLOCK && lastError != EINPROGRESS)
+                {
+                    HandleConnectionFailed("Failed to establish connection with the host: " +
+                        sockets::GetSocketErrorMessage(lastError));
+
+                    return;
+                }
+            }
+        }
+
+        void LinuxAsyncWorkerThread::HandleConnectionEvents()
+        {
+            enum { MAX_EVENTS = 16 };
+            epoll_event events[MAX_EVENTS];
+
+            int timeout = CalculateConnectionTimeout();
+
+            int res = epoll_wait(epoll, events, MAX_EVENTS, timeout);
+
+            if (res <= 0)
+                return;
+
+            for (int i = 0; i < res; ++i)
+            {
+                epoll_event& currentEvent = events[i];
+                LinuxAsyncClient* client = static_cast<LinuxAsyncClient*>(currentEvent.data.ptr);
+                if (!client)
+                    continue;
+
+                if (client == currentClient.Get())
+                {
+                    if (currentEvent.events & (EPOLLRDHUP | EPOLLERR))
+                    {
+                        HandleConnectionFailed("Can not establish connection");
+
+                        continue;
+                    }
+
+                    HandleConnectionSuccess(client);
+                }
+
+                if (currentEvent.events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP))
+                {
+                    HandleConnectionClosed(client);
+
+                    continue;
+                }
+
+                if (currentEvent.events & EPOLLIN)
+                {
+                    DataBuffer msg = client->Receive();
+                    if (msg.IsEmpty())
+                    {
+                        HandleConnectionClosed(client);
+
+                        continue;
+                    }
+
+                    clientPool.HandleMessageReceived(client->GetId(), msg);
+                }
+
+                if (currentEvent.events & EPOLLOUT)
+                {
+                    bool ok = client->ProcessSent();
+                    if (!ok)
+                    {
+                        HandleConnectionClosed(client);
+
+                        continue;
+                    }
+                }
+            }
+        }
+
+        void LinuxAsyncWorkerThread::ReportConnectionError(const EndPoint& addr, const std::string& msg)
+        {
+            IgniteError err(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
+            clientPool.HandleConnectionError(addr, err);
+        }
+
+        void LinuxAsyncWorkerThread::HandleConnectionFailed(const std::string& msg)
+        {
+            LinuxAsyncClient* client = currentClient.Get();
+            assert(client != 0);
+
+            client->StopMonitoring();
+            client->Close();
+
+            ReportConnectionError(client->GetAddress(), msg);
+
+            currentClient = SP_LinuxAsyncClient();
+            ++failedAttempts;
+        }
+
+        void LinuxAsyncWorkerThread::HandleConnectionClosed(LinuxAsyncClient *client)
+        {
+            client->StopMonitoring();
+
+            nonConnected.push_back(client->GetRange());
+
+            clientPool.CloseAndRelease(client->GetId(), 0);
+        }
+
+        void LinuxAsyncWorkerThread::HandleConnectionSuccess(LinuxAsyncClient* client)
+        {
+            nonConnected.erase(std::find(nonConnected.begin(), nonConnected.end(), client->GetRange()));
+
+            clientPool.AddClient(currentClient);
+
+            currentClient = SP_LinuxAsyncClient();
+            currentConnection.reset();
+
+            failedAttempts = 0;
+
+            clock_gettime(CLOCK_MONOTONIC, &lastConnectionTime);
+        }
+
+        int LinuxAsyncWorkerThread::CalculateConnectionTimeout() const
+        {
+            if (!ShouldInitiateNewConnection())
+                return -1;
+
+            if (lastConnectionTime.tv_sec == 0)
+                return 0;
+
+            int timeout = fibonacci10.GetValue(failedAttempts) * 1000;
+
+            timespec now;
+            clock_gettime(CLOCK_MONOTONIC, &now);
+
+            int passed = (now.tv_sec - lastConnectionTime.tv_sec) * 1000 +
+                         (now.tv_nsec - lastConnectionTime.tv_nsec) / 1000000;
+
+            timeout -= passed;
+            if (timeout < 0)
+                timeout = 0;
+
+            return timeout;
+        }
+
+        bool LinuxAsyncWorkerThread::ShouldInitiateNewConnection() const
+        {
+            return !currentClient.Get() && nonConnected.size() > minAddrs;
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.h b/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.h
new file mode 100644
index 0000000..914cb68
--- /dev/null
+++ b/modules/platforms/cpp/network/os/linux/src/network/linux_async_worker_thread.h
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_LINUX_ASYNC_WORKER_THREAD
+#define _IGNITE_NETWORK_LINUX_ASYNC_WORKER_THREAD
+
+#include <time.h>
+
+#include <stdint.h>
+#include <memory>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_handler.h>
+#include <ignite/network/end_point.h>
+#include <ignite/network/tcp_range.h>
+
+#include "network/linux_async_client.h"
+#include "network/connecting_context.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        class LinuxAsyncClientPool;
+
+        /**
+         * Async pool working thread.
+         */
+        class LinuxAsyncWorkerThread : protected common::concurrent::Thread
+        {
+        public:
+            /**
+             * Default constructor.
+             */
+            LinuxAsyncWorkerThread(LinuxAsyncClientPool& clientPool);
+
+            /**
+             * Destructor.
+             */
+            virtual ~LinuxAsyncWorkerThread();
+
+            /**
+             * Start worker thread.
+             *
+             * @param limit Connection limit.
+             * @param addrs Addresses to connect to.
+             */
+            void Start0(size_t limit, const std::vector<TcpRange>& addrs);
+
+            /**
+             * Stop thread.
+             */
+            void Stop();
+
+        private:
+            /**
+             * Run thread.
+             */
+            virtual void Run();
+
+            /**
+             * Initiate new connection process if needed.
+             */
+            void HandleNewConnections();
+
+            /**
+             * Handle epoll events.
+             */
+            void HandleConnectionEvents();
+
+            /**
+             * Handle network error during connection establishment.
+             *
+             * @param addr End point.
+             * @param msg Error message.
+             */
+            void ReportConnectionError(const EndPoint& addr, const std::string& msg);
+
+            /**
+             * Handle failed connection.
+             *
+             * @param msg Error message.
+             */
+            void HandleConnectionFailed(const std::string& msg);
+
+            /**
+             * Handle network error on established connection.
+             *
+             * @param client Client instance.
+             */
+            void HandleConnectionClosed(LinuxAsyncClient* client);
+
+            /**
+             * Handle successfully established connection.
+             *
+             * @param client Client instance.
+             */
+            void HandleConnectionSuccess(LinuxAsyncClient* client);
+
+            /**
+             * Calculate connection timeout.
+             *
+             * @return Connection timeout.
+             */
+            int CalculateConnectionTimeout() const;
+
+            /**
+             * Check whether new connection should be initiated.
+             *
+             * @return @c true if new connection should be initiated.
+             */
+            bool ShouldInitiateNewConnection() const;
+
+            /** Client pool. */
+            LinuxAsyncClientPool& clientPool;
+
+            /** Flag indicating that thread is stopping. */
+            bool stopping;
+
+            /** Client epoll file descriptor. */
+            int epoll;
+
+            /** Stop event file descriptor. */
+            int stopEvent;
+
+            /** Addresses to use for connection establishment. */
+            std::vector<TcpRange> nonConnected;
+
+            /** Connection which is currently in connecting process. */
+            std::auto_ptr<ConnectingContext> currentConnection;
+
+            /** Currently connected client. */
+            SP_LinuxAsyncClient currentClient;
+
+            /** Failed connection attempts. */
+            size_t failedAttempts;
+
+            /** Last connection time. */
+            timespec lastConnectionTime;
+
+            /** Minimal number of addresses. */
+            size_t minAddrs;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_LINUX_ASYNC_WORKER_THREAD
diff --git a/modules/platforms/cpp/network/os/linux/src/network/sockets.cpp b/modules/platforms/cpp/network/os/linux/src/network/sockets.cpp
index 08e0b5e..f5b26eb 100644
--- a/modules/platforms/cpp/network/os/linux/src/network/sockets.cpp
+++ b/modules/platforms/cpp/network/os/linux/src/network/sockets.cpp
@@ -16,6 +16,11 @@
  */
 
 #include <sys/socket.h>
+#include <sys/types.h>
+#include <netinet/tcp.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
 #include <poll.h>
 
 #include <errno.h>
@@ -55,10 +60,11 @@
                 if (error == 0)
                     return res.str();
 
-                char buffer[1024] = "";
+                char errBuf[1024] = { 0 };
 
-                if (!strerror_r(error, buffer, sizeof(buffer)))
-                    res << ", msg=" << buffer;
+                const char* errStr = strerror_r(error, errBuf, sizeof(errBuf));
+                if (errStr)
+                    res << ", msg=" << errStr;
 
                 return res.str();
             }
@@ -110,6 +116,62 @@
             {
                 return errorCode == EINTR;
             }
+
+            void TrySetSocketOptions(int socketFd, int bufSize, bool noDelay, bool outOfBand, bool keepAlive)
+            {
+                setsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&bufSize), sizeof(bufSize));
+                setsockopt(socketFd, SOL_SOCKET, SO_RCVBUF, reinterpret_cast<char*>(&bufSize), sizeof(bufSize));
+
+                int iNoDelay = noDelay ? 1 : 0;
+                setsockopt(socketFd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&iNoDelay), sizeof(iNoDelay));
+
+                int iOutOfBand = outOfBand ? 1 : 0;
+                setsockopt(socketFd, SOL_SOCKET, SO_OOBINLINE,
+                    reinterpret_cast<char*>(&iOutOfBand), sizeof(iOutOfBand));
+
+                int iKeepAlive = keepAlive ? 1 : 0;
+                int res = setsockopt(socketFd, SOL_SOCKET, SO_KEEPALIVE,
+                    reinterpret_cast<char*>(&iKeepAlive), sizeof(iKeepAlive));
+
+                if (SOCKET_ERROR == res)
+                {
+                    // There is no sense in configuring keep alive params if we faileed to set up keep alive mode.
+                    return;
+                }
+
+                // The time in seconds the connection needs to remain idle before starts sending keepalive probes.
+                enum { KEEP_ALIVE_IDLE_TIME = 60 };
+
+                // The time in seconds between individual keepalive probes.
+                enum { KEEP_ALIVE_PROBES_PERIOD = 1 };
+
+                int idleOpt = KEEP_ALIVE_IDLE_TIME;
+                int idleRetryOpt = KEEP_ALIVE_PROBES_PERIOD;
+#ifdef __APPLE__
+                setsockopt(socketFd, IPPROTO_TCP, TCP_KEEPALIVE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
+#else
+                setsockopt(socketFd, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
+#endif
+
+                setsockopt(socketFd, IPPROTO_TCP, TCP_KEEPINTVL,
+                    reinterpret_cast<char*>(&idleRetryOpt), sizeof(idleRetryOpt));
+            }
+
+            bool SetNonBlockingMode(int socketFd, bool nonBlocking)
+            {
+                int flags = fcntl(socketFd, F_GETFL, 0);
+                if (flags == -1)
+                    return false;
+
+                bool currentNonBlocking = flags & O_NONBLOCK;
+                if (nonBlocking == currentNonBlocking)
+                    return true;
+
+                flags ^= O_NONBLOCK;
+                int res = fcntl(socketFd, F_SETFL, flags);
+
+                return res != -1;
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/network/os/linux/src/network/sockets.h b/modules/platforms/cpp/network/os/linux/src/network/sockets.h
index aad7bb2..2cbac8d 100644
--- a/modules/platforms/cpp/network/os/linux/src/network/sockets.h
+++ b/modules/platforms/cpp/network/os/linux/src/network/sockets.h
@@ -76,8 +76,27 @@
              *     WaitResult::SUCCESS on success.
              */
             int WaitOnSocket(SocketHandle socket, int32_t timeout, bool rd);
+
+            /**
+             * Try and set socket options.
+             *
+             * @param socketFd Socket file descriptor.
+             * @param bufSize Buffer size.
+             * @param noDelay Set no-delay mode.
+             * @param outOfBand Set out-of-Band mode.
+             * @param keepAlive Keep alive mode.
+             */
+            void TrySetSocketOptions(int socketFd, int bufSize, bool noDelay, bool outOfBand, bool keepAlive);
+
+            /**
+             * Set non blocking mode for socket.
+             *
+             * @param socketFd Socket file descriptor.
+             * @param nonBlocking Non-blocking mode.
+             */
+            bool SetNonBlockingMode(int socketFd, bool nonBlocking);
         }
     }
 }
 
-#endif //_IGNITE_NETWORK_SOCKETS
\ No newline at end of file
+#endif //_IGNITE_NETWORK_SOCKETS
diff --git a/modules/platforms/cpp/network/os/linux/src/network/tcp_socket_client.cpp b/modules/platforms/cpp/network/os/linux/src/network/tcp_socket_client.cpp
index da5a6b0..e3c4770 100644
--- a/modules/platforms/cpp/network/os/linux/src/network/tcp_socket_client.cpp
+++ b/modules/platforms/cpp/network/os/linux/src/network/tcp_socket_client.cpp
@@ -27,6 +27,7 @@
 #include <sstream>
 
 #include <ignite/common/concurrent.h>
+#include <ignite/network/utils.h>
 
 #include <ignite/ignite_error.h>
 #include "network/tcp_socket_client.h"
@@ -66,7 +67,7 @@
             int res = getaddrinfo(hostname, strPort.c_str(), &hints, &result);
 
             if (res != 0)
-                ThrowNetworkError("Can not resolve host: " + std::string(hostname) + ":" + strPort);
+                utils::ThrowNetworkError("Can not resolve host: " + std::string(hostname) + ":" + strPort);
 
             std::string lastErrorMsg = "Failed to resolve host";
             bool isTimeout = false;
@@ -87,7 +88,8 @@
                     throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, err.c_str());
                 }
 
-                TrySetOptions();
+                sockets::TrySetSocketOptions(socketHandle, BUFFER_SIZE, true, true, true);
+                blocking = !sockets::SetNonBlockingMode(socketHandle, true);
 
                 // Connect to server.
                 res = connect(socketHandle, it->ai_addr, static_cast<int>(it->ai_addrlen));
@@ -126,7 +128,7 @@
                 if (isTimeout)
                     return false;
 
-                ThrowNetworkError(lastErrorMsg);
+                utils::ThrowNetworkError(lastErrorMsg);
             }
 
             return true;
@@ -178,46 +180,6 @@
             return blocking;
         }
 
-        void TcpSocketClient::TrySetOptions()
-        {
-            int trueOpt = 1;
-
-            int idleOpt = KEEP_ALIVE_IDLE_TIME;
-            int idleRetryOpt = KEEP_ALIVE_PROBES_PERIOD;
-            int bufSizeOpt = BUFFER_SIZE;
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&bufSizeOpt), sizeof(bufSizeOpt));
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_RCVBUF, reinterpret_cast<char*>(&bufSizeOpt), sizeof(bufSizeOpt));
-
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            blocking = false;
-
-            int flags;
-            blocking = ((flags = fcntl(socketHandle, F_GETFL, 0)) < 0) ||
-                       (fcntl(socketHandle, F_SETFL, flags | O_NONBLOCK) < 0);
-
-            int res = setsockopt(socketHandle, SOL_SOCKET, SO_KEEPALIVE,
-                reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            if (SOCKET_ERROR == res)
-            {
-                // There is no sense in configuring keep alive params if we faileed to set up keep alive mode.
-                return;
-            }
-#ifdef __APPLE__
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPALIVE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
-#else
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
-#endif
-
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPINTVL,
-                reinterpret_cast<char*>(&idleRetryOpt), sizeof(idleRetryOpt));
-        }
-
         int TcpSocketClient::WaitOnSocket(int32_t timeout, bool rd)
         {
             return sockets::WaitOnSocket(socketHandle, timeout, rd);
diff --git a/modules/platforms/cpp/network/os/win/src/network/sockets.cpp b/modules/platforms/cpp/network/os/win/src/network/sockets.cpp
index a6b6bb1..2cdbc7b 100644
--- a/modules/platforms/cpp/network/os/win/src/network/sockets.cpp
+++ b/modules/platforms/cpp/network/os/win/src/network/sockets.cpp
@@ -19,7 +19,10 @@
 
 #include <sstream>
 
+#include <ignite/common/concurrent.h>
+
 #include <ignite/network/socket_client.h>
+#include <ignite/network/utils.h>
 
 namespace ignite
 {
@@ -52,7 +55,7 @@
 
                 LPTSTR errorText = NULL;
 
-                DWORD len = FormatMessage(
+                DWORD len = FormatMessageA(
                     // use system message tables to retrieve error text
                     FORMAT_MESSAGE_FROM_SYSTEM
                     // allocate buffer on local heap for error text
@@ -64,7 +67,7 @@
                     error,
                     MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
                     // output
-                    reinterpret_cast<LPTSTR>(&errorText),
+                    reinterpret_cast<LPSTR>(&errorText),
                     // minimum size for output buffer
                     0,
                     // arguments - see note
@@ -132,6 +135,97 @@
             {
                 return errorCode == WSAEINTR;
             }
+
+            void TrySetSocketOptions(SOCKET socket, int bufSize, BOOL noDelay, BOOL outOfBand, BOOL keepAlive)
+            {
+                setsockopt(socket, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&bufSize), sizeof(bufSize));
+                setsockopt(socket, SOL_SOCKET, SO_RCVBUF, reinterpret_cast<char*>(&bufSize), sizeof(bufSize));
+
+                setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&noDelay), sizeof(noDelay));
+
+                setsockopt(socket, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast<char*>(&outOfBand), sizeof(outOfBand));
+
+                int res = setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE,
+                    reinterpret_cast<char*>(&keepAlive), sizeof(keepAlive));
+
+                if (keepAlive)
+                {
+                    if (SOCKET_ERROR == res)
+                    {
+                        // There is no sense in configuring keep alive params if we failed to set up keep alive mode.
+                        return;
+                    }
+
+                    // The time in seconds the connection needs to remain idle before starts sending keepalive probes.
+                    enum { KEEP_ALIVE_IDLE_TIME = 60 };
+
+                    // The time in seconds between individual keepalive probes.
+                    enum { KEEP_ALIVE_PROBES_PERIOD = 1 };
+
+#if defined(TCP_KEEPIDLE) && defined(TCP_KEEPINTVL)
+                    // This option is available starting with Windows 10, version 1709.
+                    DWORD idleOpt = KEEP_ALIVE_IDLE_TIME;
+                    DWORD idleRetryOpt = KEEP_ALIVE_PROBES_PERIOD;
+
+                    setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
+
+                    setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL,
+                               reinterpret_cast<char*>(&idleRetryOpt), sizeof(idleRetryOpt));
+
+#else // use old hardcore WSAIoctl
+                    // WinSock structure for KeepAlive timing settings
+                    struct tcp_keepalive settings = { 0 };
+                    settings.onoff = 1;
+                    settings.keepalivetime = KEEP_ALIVE_IDLE_TIME * 1000;
+                    settings.keepaliveinterval = KEEP_ALIVE_PROBES_PERIOD * 1000;
+
+                    // pointers for WinSock call
+                    DWORD bytesReturned;
+                    WSAOVERLAPPED overlapped;
+                    overlapped.hEvent = NULL;
+
+                    // Set KeepAlive settings
+                    WSAIoctl(
+                        socket,
+                        SIO_KEEPALIVE_VALS,
+                        &settings,
+                        sizeof(struct tcp_keepalive),
+                        NULL,
+                        0,
+                        &bytesReturned,
+                        &overlapped,
+                        NULL
+                    );
+#endif
+                }
+            }
+
+            bool SetNonBlockingMode(SOCKET socket, bool nonBlocking)
+            {
+                ULONG uTrueOpt = nonBlocking ? TRUE : FALSE;
+
+                return (ioctlsocket(socket, FIONBIO, &uTrueOpt) != SOCKET_ERROR);
+            }
+
+            void InitWsa()
+            {
+                static common::concurrent::CriticalSection initCs;
+                static bool networkInited = false;
+
+                if (!networkInited)
+                {
+                    common::concurrent::CsLockGuard lock(initCs);
+                    if (!networkInited)
+                    {
+                        WSADATA wsaData;
+
+                        networkInited = WSAStartup(MAKEWORD(2, 2), &wsaData) == 0;
+
+                        if (!networkInited)
+                            utils::ThrowNetworkError("Networking initialisation failed: " + sockets::GetLastSocketErrorMessage());
+                    }
+                }
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/network/os/win/src/network/sockets.h b/modules/platforms/cpp/network/os/win/src/network/sockets.h
index 8da1fc0..ab87b93 100644
--- a/modules/platforms/cpp/network/os/win/src/network/sockets.h
+++ b/modules/platforms/cpp/network/os/win/src/network/sockets.h
@@ -82,6 +82,32 @@
              *     WaitResult::SUCCESS on success.
              */
             int WaitOnSocket(SocketHandle socket, int32_t timeout, bool rd);
+
+            /**
+             * Try and set socket options.
+             *
+             * @param socket Socket.
+             * @param bufSize Buffer size.
+             * @param noDelay Set no-delay mode.
+             * @param outOfBand Set out-of-Band mode.
+             * @param keepAlive Keep alive mode.
+             */
+            void TrySetSocketOptions(SOCKET socket, int bufSize, BOOL noDelay, BOOL outOfBand, BOOL keepAlive);
+
+            /**
+             * Set non blocking mode for socket.
+             *
+             * @param socket Socket.
+             * @param nonBlocking Non-blocking mode.
+             */
+            bool SetNonBlockingMode(SOCKET socket, bool nonBlocking);
+
+            /**
+             * Init windows sockets.
+             *
+             * Thread-safe.
+             */
+            void InitWsa();
         }
     }
 }
diff --git a/modules/platforms/cpp/network/os/win/src/network/tcp_socket_client.cpp b/modules/platforms/cpp/network/os/win/src/network/tcp_socket_client.cpp
index 6c3c9a1..2ac211f 100644
--- a/modules/platforms/cpp/network/os/win/src/network/tcp_socket_client.cpp
+++ b/modules/platforms/cpp/network/os/win/src/network/tcp_socket_client.cpp
@@ -22,6 +22,7 @@
 #include <ignite/ignite_error.h>
 
 #include <ignite/common/concurrent.h>
+#include <ignite/network/utils.h>
 
 #include "network/tcp_socket_client.h"
 
@@ -43,23 +44,7 @@
 
         bool TcpSocketClient::Connect(const char* hostname, uint16_t port, int32_t timeout)
         {
-            static common::concurrent::CriticalSection initCs;
-            static bool networkInited = false;
-
-            // Initing networking if is not inited.
-            if (!networkInited)
-            {
-                common::concurrent::CsLockGuard lock(initCs);
-                if (!networkInited)
-                {
-                    WSADATA wsaData;
-
-                    networkInited = WSAStartup(MAKEWORD(2, 2), &wsaData) == 0;
-
-                    if (!networkInited)
-                        ThrowNetworkError("Networking initialisation failed: " + sockets::GetLastSocketErrorMessage());
-                }
-            }
+            sockets::InitWsa();
 
             InternalClose();
 
@@ -78,7 +63,7 @@
             int res = getaddrinfo(hostname, strPort.c_str(), &hints, &result);
 
             if (res != 0)
-                ThrowNetworkError("Can not resolve host: " + std::string(hostname) + ":" + strPort);
+                utils::ThrowNetworkError("Can not resolve host: " + std::string(hostname) + ":" + strPort);
 
             std::string lastErrorMsg = "Failed to resolve host";
             bool isTimeout = false;
@@ -93,9 +78,11 @@
                 socketHandle = socket(it->ai_family, it->ai_socktype, it->ai_protocol);
 
                 if (socketHandle == INVALID_SOCKET)
-                    ThrowNetworkError("Socket creation failed: " + sockets::GetLastSocketErrorMessage());
+                    utils::ThrowNetworkError("Socket creation failed: " + sockets::GetLastSocketErrorMessage());
 
-                TrySetOptions();
+                sockets::TrySetSocketOptions(socketHandle, BUFFER_SIZE, TRUE, TRUE, TRUE);
+
+                blocking = !sockets::SetNonBlockingMode(socketHandle, true);
 
                 // Connect to server.
                 res = connect(socketHandle, it->ai_addr, static_cast<int>(it->ai_addrlen));
@@ -134,7 +121,7 @@
                 if (isTimeout)
                     return false;
 
-                ThrowNetworkError(lastErrorMsg);
+                utils::ThrowNetworkError(lastErrorMsg);
             }
 
             return true;
@@ -186,68 +173,6 @@
             return blocking;
         }
 
-        void TcpSocketClient::TrySetOptions()
-        {
-            BOOL trueOpt = TRUE;
-            ULONG uTrueOpt = TRUE;
-            int bufSizeOpt = BUFFER_SIZE;
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&bufSizeOpt), sizeof(bufSizeOpt));
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_RCVBUF, reinterpret_cast<char*>(&bufSizeOpt), sizeof(bufSizeOpt));
-
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            setsockopt(socketHandle, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            blocking = ioctlsocket(socketHandle, FIONBIO, &uTrueOpt) == SOCKET_ERROR;
-
-            int res = setsockopt(socketHandle, SOL_SOCKET, SO_KEEPALIVE,
-                reinterpret_cast<char*>(&trueOpt), sizeof(trueOpt));
-
-            if (SOCKET_ERROR == res)
-            {
-                // There is no sense in configuring keep alive params if we faileed to set up keep alive mode.
-                return;
-            }
-
-            // This option is available starting with Windows 10, version 1709.
-#if defined(TCP_KEEPIDLE) && defined(TCP_KEEPINTVL)
-            DWORD idleOpt = KEEP_ALIVE_IDLE_TIME;
-            DWORD idleRetryOpt = KEEP_ALIVE_PROBES_PERIOD;
-
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast<char*>(&idleOpt), sizeof(idleOpt));
-
-            setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPINTVL,
-                reinterpret_cast<char*>(&idleRetryOpt), sizeof(idleRetryOpt));
-#else // use old hardcore WSAIoctl
-
-            // WinSock structure for KeepAlive timing settings
-            struct tcp_keepalive settings = { 0 };
-            settings.onoff = 1;
-            settings.keepalivetime = KEEP_ALIVE_IDLE_TIME * 1000;
-            settings.keepaliveinterval = KEEP_ALIVE_PROBES_PERIOD * 1000;
-
-            // pointers for WinSock call
-            DWORD bytesReturned;
-            WSAOVERLAPPED overlapped;
-            overlapped.hEvent = NULL;
-
-            // Set KeepAlive settings
-            WSAIoctl(
-                socketHandle,
-                SIO_KEEPALIVE_VALS,
-                &settings,
-                sizeof(struct tcp_keepalive),
-                NULL,
-                0,
-                &bytesReturned,
-                &overlapped,
-                NULL
-            );
-#endif
-        }
-
         int TcpSocketClient::WaitOnSocket(int32_t timeout, bool rd)
         {
             return sockets::WaitOnSocket(socketHandle, timeout, rd);
diff --git a/modules/platforms/cpp/network/os/win/src/network/utils.cpp b/modules/platforms/cpp/network/os/win/src/network/utils.cpp
index 5fde3bf..4bd8ecd 100644
--- a/modules/platforms/cpp/network/os/win/src/network/utils.cpp
+++ b/modules/platforms/cpp/network/os/win/src/network/utils.cpp
@@ -25,6 +25,7 @@
 #include <ws2ipdef.h>
 #include <ws2tcpip.h>
 #include <windows.h>
+#include <winbase.h>
 #include <iphlpapi.h>
 
 #include <ignite/ignite_error.h>
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_client.cpp b/modules/platforms/cpp/network/os/win/src/network/win_async_client.cpp
new file mode 100644
index 0000000..3028d30
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_client.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/win_async_client.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        WinAsyncClient::WinAsyncClient(SOCKET socket, const EndPoint& addr, const TcpRange& range, int32_t bufLen) :
+            bufLen(bufLen),
+            state(State::CONNECTED),
+            socket(socket),
+            id(0),
+            addr(addr),
+            range(range),
+            closeErr(IgniteError::IGNITE_SUCCESS)
+        {
+            memset(&currentSend, 0, sizeof(currentSend));
+            currentSend.kind = IoOperationKind::SEND;
+
+            memset(&currentRecv, 0, sizeof(currentRecv));
+            currentRecv.kind = IoOperationKind::RECEIVE;
+        }
+
+        WinAsyncClient::~WinAsyncClient()
+        {
+            if (State::IN_POOL == state)
+                Shutdown(0);
+
+            if (State::CLOSED != state)
+                Close();
+        }
+
+        bool WinAsyncClient::Shutdown(const IgniteError* err)
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+
+            if (State::CONNECTED != state && State::IN_POOL != state)
+                return false;
+
+            closeErr = err ? *err : IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Connection closed by application");
+
+            shutdown(socket, SD_BOTH);
+
+            state = State::SHUTDOWN;
+
+            return true;
+        }
+
+        bool WinAsyncClient::Close()
+        {
+            if (State::CLOSED == state)
+                return false;
+
+            closesocket(socket);
+
+            sendPackets.clear();
+            recvPacket = impl::interop::SP_InteropMemory();
+
+            state = State::CLOSED;
+
+            return true;
+        }
+
+        HANDLE WinAsyncClient::AddToIocp(HANDLE iocp)
+        {
+            assert(State::CONNECTED == state);
+
+            HANDLE res = CreateIoCompletionPort((HANDLE)socket, iocp, reinterpret_cast<DWORD_PTR>(this), 0);
+
+            if (!res)
+                return res;
+
+            state = State::IN_POOL;
+
+            return res;
+        }
+
+        bool WinAsyncClient::Send(const DataBuffer& data)
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+
+            if (State::CONNECTED != state && State::IN_POOL != state)
+                return false;
+
+            sendPackets.push_back(data);
+
+            if (sendPackets.size() > 1)
+                return true;
+
+            return SendNextPacketLocked();
+        }
+
+        bool WinAsyncClient::SendNextPacketLocked()
+        {
+            if (sendPackets.empty())
+                return true;
+
+            const DataBuffer& packet0 = sendPackets.front();
+            DWORD flags = 0;
+
+            WSABUF buffer;
+            buffer.buf = (CHAR*)packet0.GetData();
+            buffer.len = packet0.GetSize();
+
+            int ret = WSASend(socket, &buffer, 1, NULL, flags, &currentSend.overlapped, NULL);
+
+            return ret != SOCKET_ERROR || WSAGetLastError() == ERROR_IO_PENDING;
+        }
+
+        bool WinAsyncClient::Receive()
+        {
+            // We do not need locking on receive as we're always reading in a single thread at most.
+            // If this ever changes we'd need to add mutex locking here.
+            if (State::CONNECTED != state && State::IN_POOL != state)
+                return false;
+
+            if (!recvPacket.IsValid())
+                ClearReceiveBuffer();
+
+            impl::interop::InteropMemory& packet0 = *recvPacket.Get();
+
+            DWORD flags = 0;
+            WSABUF buffer;
+            buffer.buf = (CHAR*)packet0.Data();
+            buffer.len = (ULONG)packet0.Length();
+
+            int ret = WSARecv(socket, &buffer, 1, NULL, &flags, &currentRecv.overlapped, NULL);
+
+            return ret != SOCKET_ERROR || WSAGetLastError() == ERROR_IO_PENDING;
+        }
+
+        void WinAsyncClient::ClearReceiveBuffer()
+        {
+            using namespace impl::interop;
+
+            if (!recvPacket.IsValid())
+            {
+                recvPacket = SP_InteropMemory(new InteropUnpooledMemory(bufLen));
+                recvPacket.Get()->Length(bufLen);
+            }
+        }
+
+        DataBuffer WinAsyncClient::ProcessReceived(size_t bytes)
+        {
+            impl::interop::InteropMemory& packet0 = *recvPacket.Get();
+
+            return DataBuffer(recvPacket, 0, static_cast<int32_t>(bytes));
+        }
+
+        bool WinAsyncClient::ProcessSent(size_t bytes)
+        {
+            common::concurrent::CsLockGuard lock(sendCs);
+
+            DataBuffer& front = sendPackets.front();
+
+            front.Skip(static_cast<int32_t>(bytes));
+
+            if (front.IsEmpty())
+                sendPackets.pop_front();
+
+            return SendNextPacketLocked();
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_client.h b/modules/platforms/cpp/network/os/win/src/network/win_async_client.h
new file mode 100644
index 0000000..3e24fd4
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_client.h
@@ -0,0 +1,279 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_WIN_ASYNC_CLIENT
+#define _IGNITE_NETWORK_WIN_ASYNC_CLIENT
+
+#include "network/sockets.h"
+
+#include <stdint.h>
+#include <deque>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_handler.h>
+#include <ignite/network/codec.h>
+#include <ignite/network/end_point.h>
+#include <ignite/network/tcp_range.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Operation kind.
+         */
+        struct IoOperationKind
+        {
+            enum Type
+            {
+                SEND,
+
+                RECEIVE,
+            };
+        };
+
+        /**
+         * Represents single IO operation.
+         * Needed to be able to distinguish one operation from another.
+         */
+        struct IoOperation
+        {
+            /** Overlapped structure that should be passed to every IO operation. */
+            WSAOVERLAPPED overlapped;
+
+            /** Operation type. */
+            IoOperationKind::Type kind;
+        };
+
+        /**
+         * Windows-specific implementation of async network client.
+         */
+        class WinAsyncClient
+        {
+        public:
+            /**
+             * State.
+             */
+            struct State
+            {
+                enum Type
+                {
+                    CONNECTED,
+
+                    IN_POOL,
+
+                    SHUTDOWN,
+
+                    CLOSED,
+                };
+            };
+
+            /**
+             * Constructor.
+             *
+             * @param socket Socket.
+             * @param addr Address.
+             * @param range Range.
+             * @param bufLen Buffer length.
+             */
+            WinAsyncClient(SOCKET socket, const EndPoint& addr, const TcpRange& range, int32_t bufLen);
+
+            /**
+             * Destructor.
+             */
+            ~WinAsyncClient();
+
+            /**
+             * Shutdown client.
+             *
+             * Can be called from external threads.
+             * Can be called from WorkerThread.
+             *
+             * @param err Error message. Can be null.
+             * @return @c true if shutdown performed successfully.
+             */
+            bool Shutdown(const IgniteError* err);
+
+            /**
+             * Close client.
+             *
+             * Should not be called from external threads.
+             * Can be called from WorkerThread.
+             *
+             * @return @c true if shutdown performed successfully.
+             */
+            bool Close();
+
+            /**
+             * Add client to IOCP.
+             *
+             * @return IOCP handle on success and NULL otherwise.
+             */
+            HANDLE AddToIocp(HANDLE iocp);
+
+            /**
+             * Send packet using client.
+             *
+             * @param data Data to send.
+             * @return @c true on success.
+             */
+            bool Send(const DataBuffer& data);
+
+            /**
+             * Initiate next receive of data.
+             *
+             * @return @c true on success.
+             */
+            bool Receive();
+
+            /**
+             * Get client ID.
+             *
+             * @return Client ID.
+             */
+            uint64_t GetId() const
+            {
+                return id;
+            }
+
+            /**
+             * Set ID.
+             *
+             * @param id ID to set.
+             */
+            void SetId(uint64_t id)
+            {
+                this->id = id;
+            }
+
+            /**
+             * Get address.
+             *
+             * @return Address.
+             */
+            const EndPoint& GetAddress() const
+            {
+                return addr;
+            }
+
+            /**
+             * Get range.
+             *
+             * @return Range.
+             */
+            const TcpRange& GetRange() const
+            {
+                return range;
+            }
+
+            /**
+             * Check whether client is closed.
+             *
+             * @return @c true if closed.
+             */
+            bool IsClosed() const
+            {
+                return socket == NULL;
+            }
+
+            /**
+             * Process sent data.
+             *
+             * @param bytes Bytes.
+             * @return @c true on success.
+             */
+            bool ProcessSent(size_t bytes);
+
+            /**
+             * Process received bytes.
+             *
+             * @param bytes Number of received bytes.
+             */
+            DataBuffer ProcessReceived(size_t bytes);
+
+            /**
+             * Get closing error for the connection. Can be IGNITE_SUCCESS.
+             *
+             * @return Connection error.
+             */
+            const IgniteError& GetCloseError() const
+            {
+                return closeErr;
+            }
+
+        private:
+
+            /**
+             * Clears client's receive buffer.
+             *
+             * @return Data received so far.
+             */
+            void ClearReceiveBuffer();
+
+            /**
+             * Send next packet in queue.
+             *
+             * @warning Can only be called when holding sendCs lock.
+             * @return @c true on success.
+             */
+            bool SendNextPacketLocked();
+
+            /** Buffer length. */
+            int32_t bufLen;
+
+            /** Client state. */
+            State::Type state;
+
+            /** Socket. */
+            SOCKET socket;
+
+            /** Connection ID. */
+            uint64_t id;
+
+            /** Server end point. */
+            EndPoint addr;
+
+            /** Address range associated with current connection. */
+            TcpRange range;
+
+            /** Current send operation. */
+            IoOperation currentSend;
+
+            /** Packets that should be sent. */
+            std::deque<DataBuffer> sendPackets;
+
+            /** Send critical section. */
+            common::concurrent::CriticalSection sendCs;
+
+            /** Current receive operation. */
+            IoOperation currentRecv;
+
+            /** Packet that is currently received. */
+            impl::interop::SP_InteropMemory recvPacket;
+
+            /** Closing error. */
+            IgniteError closeErr;
+        };
+
+        /** Shared pointer to async client. */
+        typedef common::concurrent::SharedPointer<WinAsyncClient> SP_WinAsyncClient;
+    }
+}
+
+#endif //_IGNITE_NETWORK_WIN_ASYNC_CLIENT
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.cpp b/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.cpp
new file mode 100644
index 0000000..d634cfd
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/win_async_client_pool.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        WinAsyncClientPool::WinAsyncClientPool() :
+            stopping(true),
+            asyncHandler(0),
+            connectingThread(),
+            workerThread(),
+            idGen(0),
+            iocp(NULL),
+            clientsCs(),
+            clientIdMap()
+        {
+            // No-op.
+        }
+
+        WinAsyncClientPool::~WinAsyncClientPool()
+        {
+            InternalStop();
+        }
+
+        void WinAsyncClientPool::Start(const std::vector<TcpRange>& addrs, uint32_t connLimit)
+        {
+            if (!stopping)
+                throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Client pool is already started");
+
+            stopping = false;
+
+            sockets::InitWsa();
+
+            iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
+            if (iocp == NULL)
+                common::ThrowLastSystemError("Failed to create IOCP instance");
+
+            try
+            {
+                connectingThread.Start(*this, connLimit, addrs);
+                workerThread.Start(*this, iocp);
+            }
+            catch (...)
+            {
+                Stop();
+
+                throw;
+            }
+        }
+
+        void WinAsyncClientPool::Stop()
+        {
+            InternalStop();
+        }
+
+        void WinAsyncClientPool::InternalStop()
+        {
+            stopping = true;
+            connectingThread.Stop();
+
+            {
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                std::map<uint64_t, SP_WinAsyncClient>::iterator it;
+                for (it = clientIdMap.begin(); it != clientIdMap.end(); ++it)
+                {
+                    WinAsyncClient& client = *it->second.Get();
+
+                    client.Shutdown(0);
+                    client.Close();
+                }
+            }
+
+            workerThread.Stop();
+
+            CloseHandle(iocp);
+            iocp = NULL;
+
+            clientIdMap.clear();
+        }
+
+        bool WinAsyncClientPool::AddClient(SP_WinAsyncClient& client)
+        {
+            uint64_t id;
+            {
+                WinAsyncClient& clientRef = *client.Get();
+
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                if (stopping)
+                    return false;
+
+                id = ++idGen;
+                clientRef.SetId(id);
+
+                HANDLE iocp0 = clientRef.AddToIocp(iocp);
+                if (iocp0 == NULL)
+                    common::ThrowLastSystemError("Can not add socket to IOCP");
+
+                iocp = iocp0;
+
+                clientIdMap[id] = client;
+            }
+
+            PostQueuedCompletionStatus(iocp, 0, reinterpret_cast<ULONG_PTR>(client.Get()), 0);
+
+            return true;
+        }
+
+        void WinAsyncClientPool::HandleConnectionError(const EndPoint &addr, const IgniteError &err)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionError(addr, err);
+        }
+
+        void WinAsyncClientPool::HandleConnectionSuccess(const EndPoint &addr, uint64_t id)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionSuccess(addr, id);
+        }
+
+        void WinAsyncClientPool::HandleConnectionClosed(uint64_t id, const IgniteError *err)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnConnectionClosed(id, err);
+        }
+
+        void WinAsyncClientPool::HandleMessageReceived(uint64_t id, const DataBuffer &msg)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnMessageReceived(id, msg);
+        }
+
+        void WinAsyncClientPool::HandleMessageSent(uint64_t id)
+        {
+            AsyncHandler* asyncHandler0 = asyncHandler;
+            if (asyncHandler0)
+                asyncHandler0->OnMessageSent(id);
+        }
+
+        bool WinAsyncClientPool::Send(uint64_t id, const DataBuffer& data)
+        {
+            if (stopping)
+                return false;
+
+            SP_WinAsyncClient client = FindClient(id);
+            if (!client.IsValid())
+                return false;
+
+            return client.Get()->Send(data);
+        }
+
+        void WinAsyncClientPool::CloseAndRelease(uint64_t id, const IgniteError* err)
+        {
+            SP_WinAsyncClient client;
+            {
+                common::concurrent::CsLockGuard lock(clientsCs);
+
+                std::map<uint64_t, SP_WinAsyncClient>::iterator it = clientIdMap.find(id);
+                if (it == clientIdMap.end())
+                    return;
+
+                client = it->second;
+
+                clientIdMap.erase(it);
+            }
+
+            bool closed = client.Get()->Close();
+            if (closed)
+            {
+                connectingThread.NotifyFreeAddress(client.Get()->GetRange());
+
+                IgniteError err0(client.Get()->GetCloseError());
+                if (err0.GetCode() == IgniteError::IGNITE_SUCCESS)
+                    err0 = IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, "Connection closed by server");
+
+                if (!err)
+                    err = &err0;
+
+                HandleConnectionClosed(id, err);
+            }
+        }
+
+        void WinAsyncClientPool::Close(uint64_t id, const IgniteError* err)
+        {
+            SP_WinAsyncClient client = FindClient(id);
+            if (client.IsValid() && !client.Get()->IsClosed())
+                client.Get()->Shutdown(err);
+        }
+
+        SP_WinAsyncClient WinAsyncClientPool::FindClient(uint64_t id) const
+        {
+            common::concurrent::CsLockGuard lock(clientsCs);
+
+            return FindClientLocked(id);
+        }
+
+        SP_WinAsyncClient WinAsyncClientPool::FindClientLocked(uint64_t id) const
+        {
+            std::map<uint64_t, SP_WinAsyncClient>::const_iterator it = clientIdMap.find(id);
+            if (it == clientIdMap.end())
+                return SP_WinAsyncClient();
+
+            return it->second;
+        }
+
+        void WinAsyncClientPool::SetHandler(AsyncHandler *handler)
+        {
+            asyncHandler = handler;
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.h b/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.h
new file mode 100644
index 0000000..493b65f
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_client_pool.h
@@ -0,0 +1,211 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_WIN_ASYNC_CLIENT_POOL
+#define _IGNITE_NETWORK_WIN_ASYNC_CLIENT_POOL
+
+#include <stdint.h>
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_client_pool.h>
+#include <ignite/network/async_handler.h>
+#include <ignite/network/tcp_range.h>
+
+#include "network/win_async_client.h"
+#include "network/win_async_connecting_thread.h"
+#include "network/win_async_worker_thread.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Windows-specific implementation of asynchronous client pool.
+         */
+        class WinAsyncClientPool : public AsyncClientPool
+        {
+        public:
+            /**
+             * Constructor
+             *
+             * @param handler Upper level event handler.
+             */
+            WinAsyncClientPool();
+
+            /**
+             * Destructor.
+             */
+            virtual ~WinAsyncClientPool();
+
+            /**
+             * Start internal thread that establishes connections to provided addresses and asynchronously sends and
+             * receives messages from them. Function returns either when thread is started and first connection is
+             * established or failure happened.
+             *
+             * @param addrs Addresses to connect to.
+             * @param connLimit Connection upper limit. Zero means limit is disabled.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual void Start(const std::vector<TcpRange>& addrs, uint32_t connLimit);
+
+            /**
+             * Close all established connections and stops handling thread.
+             */
+            virtual void Stop();
+
+            /**
+             * Set handler.
+             *
+             * @param handler Handler to set.
+             */
+            virtual void SetHandler(AsyncHandler *handler);
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data);
+
+            /**
+             * Closes specified connection if it's established. Connection to the specified address is planned for
+             * re-connect. Event is issued to the handler with specified error.
+             *
+             * @param id Client ID.
+             */
+            virtual void Close(uint64_t id, const IgniteError* err);
+
+            /**
+             * Closes and releases memory allocated for client with specified ID.
+             * Error is reported to handler.
+             *
+             * @param id Client ID.
+             * @param err Error to report. May be null.
+             * @return @c true if connection with specified ID was found.
+             */
+            void CloseAndRelease(uint64_t id, const IgniteError* err);
+
+            /**
+             * Add client to connection map. Notify user.
+             *
+             * @param client Client.
+             * @return Client ID.
+             */
+            bool AddClient(SP_WinAsyncClient& client);
+
+            /**
+             * Handle error during connection establishment.
+             *
+             * @param addr Connection address.
+             * @param err Error.
+             */
+            void HandleConnectionError(const EndPoint& addr, const IgniteError& err);
+
+            /**
+             * Handle successful connection establishment.
+             *
+             * @param addr Address of the new connection.
+             * @param id Connection ID.
+             */
+            void HandleConnectionSuccess(const EndPoint& addr, uint64_t id);
+
+            /**
+             * Handle error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            void HandleConnectionClosed(uint64_t id, const IgniteError* err);
+
+            /**
+             * Handle new message.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            void HandleMessageReceived(uint64_t id, const DataBuffer& msg);
+
+            /**
+             * Handle sent message event.
+             *
+             * @param id Async client ID.
+             */
+            void HandleMessageSent(uint64_t id);
+
+        private:
+             /**
+             * Close all established connections and stops handling threads.
+             */
+            void InternalStop();
+
+            /**
+             * Find client by ID.
+             *
+             * @param id Client ID.
+             * @return Client. Null pointer if is not found.
+             */
+            SP_WinAsyncClient FindClient(uint64_t id) const;
+
+            /**
+             * Find client by ID.
+             *
+             * @warning Should only be called with clientsCs lock held.
+             * @param id Client ID.
+             * @return Client. Null pointer if is not found.
+             */
+            SP_WinAsyncClient FindClientLocked(uint64_t id) const;
+
+            /** Flag indicating that pool is stopping. */
+            volatile bool stopping;
+
+            /** Event handler. */
+            AsyncHandler* asyncHandler;
+
+            /** Connecting thread. */
+            WinAsyncConnectingThread connectingThread;
+
+            /** Internal thread. */
+            WinAsyncWorkerThread workerThread;
+
+            /** ID counter. */
+            uint64_t idGen;
+
+            /** IO Completion Port. Windows-specific primitive for asynchronous IO. */
+            HANDLE iocp;
+
+            /** Clients critical section. */
+            mutable common::concurrent::CriticalSection clientsCs;
+
+            /** Client mapping ID -> client */
+            std::map<uint64_t, SP_WinAsyncClient> clientIdMap;
+        };
+
+        // Type alias
+        typedef common::concurrent::SharedPointer<network::AsyncClientPool> SP_AsyncClientPool;
+    }
+}
+
+#endif //_IGNITE_NETWORK_WIN_ASYNC_CLIENT_POOL
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.cpp b/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.cpp
new file mode 100644
index 0000000..4ceb4a6
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.cpp
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ */
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/win_async_client_pool.h"
+#include "network/win_async_connecting_thread.h"
+
+namespace
+{
+    ignite::common::FibonacciSequence<10> fibonacci10;
+}
+
+namespace ignite
+{
+    namespace network
+    {
+        WinAsyncConnectingThread::WinAsyncConnectingThread() :
+            clientPool(0),
+            stopping(false),
+            failedAttempts(0),
+            minAddrs(0),
+            addrsCs(),
+            connectNeeded(),
+            nonConnected()
+        {
+            // No-op.
+        }
+
+        void WinAsyncConnectingThread::Run()
+        {
+            assert(clientPool != 0);
+
+            while (!stopping)
+            {
+                TcpRange range = GetRandomAddress();
+
+                if (stopping || range.IsEmpty())
+                    break;
+
+                SP_WinAsyncClient client = TryConnect(range);
+
+                if (!client.IsValid())
+                {
+                    ++failedAttempts;
+
+                    DWORD msToWait = static_cast<DWORD>(1000 * fibonacci10.GetValue(failedAttempts));
+                    if (msToWait)
+                        Sleep(msToWait);
+
+                    continue;
+                }
+
+                failedAttempts = 0;
+
+                if (stopping)
+                {
+                    client.Get()->Close();
+
+                    return;
+                }
+
+                try
+                {
+                    bool added = clientPool->AddClient(client);
+
+                    if (!added)
+                    {
+                        client.Get()->Close();
+
+                        continue;
+                    }
+
+                    common::concurrent::CsLockGuard lock(addrsCs);
+                    std::vector<TcpRange>::iterator it = std::find(nonConnected.begin(), nonConnected.end(), range);
+                    if (it != nonConnected.end())
+                        nonConnected.erase(it);
+                }
+                catch (const IgniteError& err)
+                {
+                    client.Get()->Close();
+
+                    clientPool->HandleConnectionError(client.Get()->GetAddress(), err);
+
+                    continue;
+                }
+            }
+        }
+
+        void WinAsyncConnectingThread::NotifyFreeAddress(const TcpRange &range)
+        {
+            common::concurrent::CsLockGuard lock(addrsCs);
+
+            nonConnected.push_back(range);
+            connectNeeded.NotifyOne();
+        }
+
+        void WinAsyncConnectingThread::Start(
+            WinAsyncClientPool& clientPool0,
+            size_t limit,
+            const std::vector<TcpRange>& addrs)
+        {
+            stopping = false;
+            clientPool = &clientPool0;
+            failedAttempts = 0;
+            nonConnected = addrs;
+
+            if (!limit || limit > addrs.size())
+                minAddrs = 0;
+            else
+                minAddrs = addrs.size() - limit;
+
+            Thread::Start();
+        }
+
+        void WinAsyncConnectingThread::Stop()
+        {
+            stopping = true;
+
+            {
+                common::concurrent::CsLockGuard lock(addrsCs);
+                connectNeeded.NotifyOne();
+            }
+
+            Join();
+            nonConnected.clear();
+        }
+
+        SP_WinAsyncClient WinAsyncConnectingThread::TryConnect(const TcpRange& range)
+        {
+            for (uint16_t port = range.port; port <= (range.port + range.range); ++port)
+            {
+                EndPoint addr(range.host, port);
+                try
+                {
+                    SOCKET socket = TryConnect(addr);
+
+                    return SP_WinAsyncClient(new WinAsyncClient(socket, addr, range, BUFFER_SIZE));
+                }
+                catch (const IgniteError&)
+                {
+                    // No-op.
+                }
+            }
+
+            return SP_WinAsyncClient();
+        }
+
+        SOCKET WinAsyncConnectingThread::TryConnect(const EndPoint& addr)
+        {
+            addrinfo hints;
+            memset(&hints, 0, sizeof(hints));
+
+            hints.ai_family = AF_UNSPEC;
+            hints.ai_socktype = SOCK_STREAM;
+            hints.ai_protocol = IPPROTO_TCP;
+
+            std::stringstream converter;
+            converter << addr.port;
+            std::string strPort = converter.str();
+
+            // Resolve the server address and port
+            addrinfo *result = NULL;
+            int res = getaddrinfo(addr.host.c_str(), strPort.c_str(), &hints, &result);
+
+            if (res != 0)
+                utils::ThrowNetworkError("Can not resolve host: " + addr.host + ":" + strPort);
+
+            std::string lastErrorMsg = "Failed to resolve host";
+
+            SOCKET socket = INVALID_SOCKET;
+
+            // Attempt to connect to an address until one succeeds
+            for (addrinfo *it = result; it != NULL; it = it->ai_next)
+            {
+                lastErrorMsg = "Failed to establish connection with the host";
+
+                socket = WSASocket(it->ai_family, it->ai_socktype, it->ai_protocol, NULL, 0, WSA_FLAG_OVERLAPPED);
+
+                if (socket == INVALID_SOCKET)
+                    utils::ThrowNetworkError("Socket creation failed: " + sockets::GetLastSocketErrorMessage());
+
+                sockets::TrySetSocketOptions(socket, BUFFER_SIZE, TRUE, TRUE, TRUE);
+
+                // Connect to server.
+                res = WSAConnect(socket, it->ai_addr, static_cast<int>(it->ai_addrlen), NULL, NULL, NULL, NULL);
+                if (SOCKET_ERROR == res)
+                {
+                    closesocket(socket);
+                    socket = INVALID_SOCKET;
+
+                    int lastError = WSAGetLastError();
+
+                    if (lastError != WSAEWOULDBLOCK)
+                    {
+                        lastErrorMsg.append(": ").append(sockets::GetSocketErrorMessage(lastError));
+
+                        continue;
+                    }
+                }
+
+                break;
+            }
+
+            freeaddrinfo(result);
+
+            if (socket == INVALID_SOCKET)
+                utils::ThrowNetworkError(lastErrorMsg);
+
+            return socket;
+        }
+
+        TcpRange WinAsyncConnectingThread::GetRandomAddress() const
+        {
+            common::concurrent::CsLockGuard lock(addrsCs);
+
+            if (stopping)
+                return TcpRange();
+
+            while (nonConnected.size() <= minAddrs)
+            {
+                connectNeeded.Wait(addrsCs);
+
+                if (stopping)
+                    return TcpRange();
+            }
+
+            size_t idx = rand() % nonConnected.size();
+            TcpRange range = nonConnected.at(idx);
+
+            lock.Reset();
+
+            return range;
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.h b/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.h
new file mode 100644
index 0000000..90dbbc7
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_connecting_thread.h
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_WIN_ASYNC_CONNECTING_THREAD
+#define _IGNITE_NETWORK_WIN_ASYNC_CONNECTING_THREAD
+
+#include <stdint.h>
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/concurrent.h>
+#include <ignite/impl/interop/interop_memory.h>
+
+#include <ignite/network/async_client_pool.h>
+#include <ignite/network/async_handler.h>
+#include <ignite/network/tcp_range.h>
+
+#include "network/win_async_client.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        class WinAsyncClientPool;
+
+        /**
+         * Async pool connecting thread.
+         */
+        class WinAsyncConnectingThread : protected common::concurrent::Thread
+        {
+            /** Send and receive buffers size. */
+            enum { BUFFER_SIZE = 0x10000 };
+
+        public:
+            /**
+             * Constructor.
+             */
+            explicit WinAsyncConnectingThread();
+
+            /**
+             * Start thread.
+             *
+             * @param clientPool Client pool.
+             * @param limit Connection limit.
+             * @param addrs Addresses.
+             */
+            void Start(WinAsyncClientPool& clientPool, size_t limit, const std::vector<TcpRange>& addrs);
+
+            /**
+             * Stop thread.
+             */
+            void Stop();
+
+            /**
+             * Notify about new address available for connection.
+             *
+             * @param range Address range.
+             */
+            void NotifyFreeAddress(const TcpRange &range);
+
+        private:
+            /**
+             * Run thread.
+             */
+            virtual void Run();
+
+            /**
+             * Try establish connection to address in the range.
+             * @param range TCP range.
+             * @return New client.
+             */
+            static SP_WinAsyncClient TryConnect(const TcpRange& range);
+
+            /**
+             * Try establish connection to address.
+             * @param addr Address.
+             * @return Socket.
+             */
+            static SOCKET TryConnect(const EndPoint& addr);
+
+            /**
+             * Get random address.
+             *
+             * @warning Will block if no addresses are available for connect.
+             * @return @c true if a new connection should be established.
+             */
+            TcpRange GetRandomAddress() const;
+
+            /** Client pool. */
+            WinAsyncClientPool* clientPool;
+
+            /** Flag to signal that thread is stopping. */
+            volatile bool stopping;
+
+            /** Failed connection attempts. */
+            size_t failedAttempts;
+
+            /** Minimal number of addresses. */
+            size_t minAddrs;
+
+            /** Addresses critical section. */
+            mutable common::concurrent::CriticalSection addrsCs;
+
+            /** Condition variable, which signalled when new connect is needed. */
+            mutable common::concurrent::ConditionVariable connectNeeded;
+
+            /** Addresses to use for connection establishment. */
+            std::vector<TcpRange> nonConnected;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_WIN_ASYNC_CONNECTING_THREAD
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.cpp b/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.cpp
new file mode 100644
index 0000000..d516f84
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.cpp
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/utils.h>
+
+#include "network/sockets.h"
+#include "network/win_async_client.h"
+#include "network/win_async_client_pool.h"
+#include "network/win_async_worker_thread.h"
+
+namespace
+{
+    ignite::common::FibonacciSequence<10> fibonacci10;
+}
+
+namespace ignite
+{
+    namespace network
+    {
+        WinAsyncWorkerThread::WinAsyncWorkerThread() :
+            stopping(false),
+            clientPool(0),
+            iocp(NULL)
+        {
+            // No-op.
+        }
+
+        void WinAsyncWorkerThread::Start(WinAsyncClientPool& clientPool0, HANDLE iocp0)
+        {
+            assert(iocp0 != NULL);
+            iocp = iocp0;
+            clientPool = &clientPool0;
+
+            Thread::Start();
+        }
+
+        void WinAsyncWorkerThread::Run()
+        {
+            assert(clientPool != 0);
+
+            while (!stopping)
+            {
+                DWORD bytesTransferred = 0;
+                ULONG_PTR key = NULL;
+                LPOVERLAPPED overlapped = NULL;
+
+                BOOL ok = GetQueuedCompletionStatus(iocp, &bytesTransferred, &key, &overlapped, INFINITE);
+
+                if (stopping)
+                    break;
+
+                if (!key)
+                    continue;
+
+                WinAsyncClient* client = reinterpret_cast<WinAsyncClient*>(key);
+
+                if (!ok || (0 != overlapped && 0 == bytesTransferred))
+                {
+                    clientPool->CloseAndRelease(client->GetId(), 0);
+
+                    continue;
+                }
+
+                if (!overlapped)
+                {
+                    // This mean new client is connected.
+                    clientPool->HandleConnectionSuccess(client->GetAddress(), client->GetId());
+
+                    bool success = client->Receive();
+                    if (!success)
+                        clientPool->CloseAndRelease(client->GetId(), 0);
+
+                    continue;
+                }
+
+                try
+                {
+                    IoOperation* operation = reinterpret_cast<IoOperation*>(overlapped);
+                    switch (operation->kind)
+                    {
+                        case IoOperationKind::SEND:
+                        {
+                            bool success = client->ProcessSent(bytesTransferred);
+
+                            if (!success)
+                                clientPool->CloseAndRelease(client->GetId(), 0);
+
+                            clientPool->HandleMessageSent(client->GetId());
+
+                            break;
+                        }
+
+                        case IoOperationKind::RECEIVE:
+                        {
+                            DataBuffer data = client->ProcessReceived(bytesTransferred);
+
+                            if (!data.IsEmpty())
+                                clientPool->HandleMessageReceived(client->GetId(), data);
+
+                            bool success = client->Receive();
+
+                            if (!success)
+                                clientPool->CloseAndRelease(client->GetId(), 0);
+
+                            break;
+                        }
+
+                        default:
+                            break;
+                    }
+                }
+                catch (const IgniteError& err)
+                {
+                    clientPool->CloseAndRelease(client->GetId(), &err);
+                }
+            }
+        }
+
+        void WinAsyncWorkerThread::Stop()
+        {
+            stopping = true;
+
+            PostQueuedCompletionStatus(iocp, 0, 0, 0);
+
+            Join();
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.h b/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.h
new file mode 100644
index 0000000..c4bc575
--- /dev/null
+++ b/modules/platforms/cpp/network/os/win/src/network/win_async_worker_thread.h
@@ -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.
+ */
+
+#ifndef _IGNITE_NETWORK_WIN_ASYNC_WORKER_THREAD
+#define _IGNITE_NETWORK_WIN_ASYNC_WORKER_THREAD
+
+#include <stdint.h>
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/concurrent.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /** Windows async client pool. */
+        class WinAsyncClientPool;
+
+        /**
+         * Async pool worker thread.
+         */
+        class WinAsyncWorkerThread : protected common::concurrent::Thread
+        {
+        public:
+            /**
+             * Constructor.
+             */
+            explicit WinAsyncWorkerThread();
+
+            /**
+             * Start thread.
+             *
+             * @param clientPool Client pool.
+             * @param iocp Valid IOCP instance handle.
+             */
+            void Start(WinAsyncClientPool& clientPool, HANDLE iocp);
+
+            /**
+             * Stop thread.
+             */
+            void Stop();
+
+        private:
+            /**
+             * Run thread.
+             */
+            virtual void Run();
+
+            /** Flag to signal that thread should stop. */
+            volatile bool stopping;
+
+            /** Client pool. */
+            WinAsyncClientPool* clientPool;
+
+            /** IO Completion Port. Windows-specific primitive for asynchronous IO. */
+            HANDLE iocp;
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_WIN_ASYNC_WORKER_THREAD
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/src/network/async_client_pool_adapter.cpp b/modules/platforms/cpp/network/src/network/async_client_pool_adapter.cpp
new file mode 100644
index 0000000..06bec2a
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/async_client_pool_adapter.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#include "network/async_client_pool_adapter.h"
+#include "network/error_handling_filter.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        AsyncClientPoolAdapter::AsyncClientPoolAdapter(
+            const std::vector<SP_DataFilter> &filters0,
+            const SP_AsyncClientPool& pool0
+        ) :
+            filters(filters0),
+            pool(pool0),
+            sink(pool.Get()),
+            handler(0)
+        {
+            filters.insert(filters.begin(), SP_DataFilter(new ErrorHandlingFilter()));
+
+            for (std::vector<SP_DataFilter>::iterator it = filters.begin(); it != filters.end(); ++it)
+            {
+                it->Get()->SetSink(sink);
+                sink = it->Get();
+            }
+        }
+
+        void AsyncClientPoolAdapter::Start(const std::vector<TcpRange>& addrs, uint32_t connLimit)
+        {
+            pool.Get()->Start(addrs, connLimit);
+        }
+
+        void AsyncClientPoolAdapter::Stop()
+        {
+            pool.Get()->Stop();
+        }
+
+        void AsyncClientPoolAdapter::SetHandler(AsyncHandler* handler0)
+        {
+            handler = handler0;
+            for (std::vector<SP_DataFilter>::reverse_iterator it = filters.rbegin(); it != filters.rend(); ++it)
+            {
+                it->Get()->SetHandler(handler);
+                handler = it->Get();
+            }
+
+            pool.Get()->SetHandler(handler);
+        }
+
+        bool AsyncClientPoolAdapter::Send(uint64_t id, const DataBuffer& data)
+        {
+            return sink->Send(id, data);
+        }
+
+        void AsyncClientPoolAdapter::Close(uint64_t id, const IgniteError* err)
+        {
+            sink->Close(id, err);
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/async_client_pool_adapter.h b/modules/platforms/cpp/network/src/network/async_client_pool_adapter.h
new file mode 100644
index 0000000..48ac025
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/async_client_pool_adapter.h
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_ASYNC_CLIENT_POOL_ADAPTER
+#define _IGNITE_NETWORK_ASYNC_CLIENT_POOL_ADAPTER
+
+#include <ignite/network/async_client_pool.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Asynchronous client pool adapter.
+         */
+        class AsyncClientPoolAdapter : public AsyncClientPool
+        {
+        public:
+            /**
+             * Constructor.
+             *
+             * @param filters Filters.
+             * @param pool Client pool.
+             */
+            AsyncClientPoolAdapter(const std::vector<SP_DataFilter>& filters, const SP_AsyncClientPool& pool);
+
+            /**
+             * Destructor.
+             */
+            virtual ~AsyncClientPoolAdapter()
+            {
+                // No-op.
+            }
+
+            /**
+             * Start internal thread that establishes connections to provided addresses and asynchronously sends and
+             * receives messages from them. Function returns either when thread is started and first connection is
+             * established or failure happened.
+             *
+             * @param addrs Addresses to connect to.
+             * @param connLimit Connection upper limit. Zero means limit is disabled.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual void Start(const std::vector<TcpRange>& addrs, uint32_t connLimit);
+
+            /**
+             * Close all established connections and stops handling threads.
+             */
+            virtual void Stop();
+
+            /**
+             * Set handler.
+             *
+             * @param handler Handler to set.
+             */
+            virtual void SetHandler(AsyncHandler *handler);
+
+            /**
+             * Send data to specific established connection.
+             *
+             * @param id Client ID.
+             * @param data Data to be sent.
+             * @return @c true if connection is present and @c false otherwise.
+             *
+             * @throw IgniteError on error.
+             */
+            virtual bool Send(uint64_t id, const DataBuffer& data);
+
+            /**
+             * Closes specified connection if it's established. Connection to the specified address is planned for
+             * re-connect. Error is reported to handler.
+             *
+             * @param id Client ID.
+             */
+            virtual void Close(uint64_t id, const IgniteError* err);
+
+        private:
+            /** Filters. */
+            std::vector<SP_DataFilter> filters;
+
+            /** Underlying pool. */
+            SP_AsyncClientPool pool;
+
+            /** Lower level data sink. */
+            DataSink* sink;
+
+            /** Upper level event handler. */
+            AsyncHandler* handler;
+        };
+
+        // Type alias
+        typedef common::concurrent::SharedPointer<AsyncClientPoolAdapter> SP_AsyncClientPoolAdapter;
+    }
+}
+
+#endif //_IGNITE_NETWORK_ASYNC_CLIENT_POOL_ADAPTER
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/src/network/codec_data_filter.cpp b/modules/platforms/cpp/network/src/network/codec_data_filter.cpp
new file mode 100644
index 0000000..d5d6c42
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/codec_data_filter.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+#include <ignite/network/codec_data_filter.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        CodecDataFilter::CodecDataFilter(const SP_CodecFactory &factory) :
+            codecFactory(factory),
+            codecs(new CodecMap()),
+            codecsCs()
+        {
+            // No-op.
+        }
+
+        CodecDataFilter::~CodecDataFilter()
+        {
+            delete codecs;
+        }
+
+        bool CodecDataFilter::Send(uint64_t id, const DataBuffer &data)
+        {
+            SP_Codec codec = FindCodec(id);
+            if (!codec.IsValid())
+                return false;
+
+            DataBuffer data0(data);
+            while (true)
+            {
+                DataBuffer out = codec.Get()->Encode(data0);
+
+                if (out.IsEmpty())
+                    break;
+
+                DataFilterAdapter::Send(id, out);
+            }
+
+            return true;
+        }
+
+        void CodecDataFilter::OnConnectionSuccess(const EndPoint &addr, uint64_t id)
+        {
+            {
+                common::concurrent::CsLockGuard lock(codecsCs);
+
+                codecs->insert(std::make_pair(id, codecFactory.Get()->Build()));
+            }
+
+            DataFilterAdapter::OnConnectionSuccess(addr, id);
+        }
+
+        void CodecDataFilter::OnConnectionClosed(uint64_t id, const IgniteError *err)
+        {
+            {
+                common::concurrent::CsLockGuard lock(codecsCs);
+
+                codecs->erase(id);
+            }
+
+            DataFilterAdapter::OnConnectionClosed(id, err);
+        }
+
+        void CodecDataFilter::OnMessageReceived(uint64_t id, const DataBuffer &msg)
+        {
+            SP_Codec codec = FindCodec(id);
+            if (!codec.IsValid())
+                return;
+
+            DataBuffer msg0(msg);
+            while (true)
+            {
+                DataBuffer out = codec.Get()->Decode(msg0);
+
+                if (out.IsEmpty())
+                    break;
+
+                DataFilterAdapter::OnMessageReceived(id, out);
+            }
+        }
+
+        SP_Codec CodecDataFilter::FindCodec(uint64_t id)
+        {
+            common::concurrent::CsLockGuard lock(codecsCs);
+
+            std::map<uint64_t, SP_Codec>::iterator it = codecs->find(id);
+            if (it == codecs->end())
+                return SP_Codec();
+
+            return it->second;
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/data_buffer.cpp b/modules/platforms/cpp/network/src/network/data_buffer.cpp
new file mode 100644
index 0000000..9caf5ca
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/data_buffer.cpp
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+#include <cstring>
+
+#include <ignite/ignite_error.h>
+#include <ignite/network/data_buffer.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        DataBuffer::DataBuffer() :
+            position(0),
+            length(0),
+            data()
+        {
+            // No-op.
+        }
+
+        DataBuffer::DataBuffer(const impl::interop::SP_ConstInteropMemory& data0) :
+            position(0),
+            length(data0.Get()->Length()),
+            data(data0)
+        {
+            // No-op.
+        }
+
+        DataBuffer::DataBuffer(const impl::interop::SP_ConstInteropMemory& data0, int32_t pos, int32_t len) :
+            position(pos),
+            length(len),
+            data(data0)
+        {
+            // No-op.
+        }
+
+        void DataBuffer::Consume(int8_t *dst, int32_t size)
+        {
+            if (!size)
+                return;
+
+            if (size < 0)
+                throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
+                    "Codec error: Can not read negative number of bytes");
+
+            if (GetSize() < size)
+                throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
+                    "Codec error: Not enough data to read data from buffer");
+
+            std::memcpy(dst, data.Get()->Data() + position, size);
+            Advance(size);
+        }
+
+        const int8_t *DataBuffer::GetData() const
+        {
+            return data.Get()->Data() + position;
+        }
+
+        int32_t DataBuffer::GetSize() const
+        {
+            if (!data.IsValid())
+                return 0;
+
+            return length - position;
+        }
+
+        bool DataBuffer::IsEmpty() const
+        {
+            return GetSize() <= 0;
+        }
+
+        DataBuffer DataBuffer::ConsumeEntirely()
+        {
+            DataBuffer res(*this);
+            Advance(GetSize());
+
+            return res;
+        }
+
+        void DataBuffer::Advance(int32_t val)
+        {
+            position += val;
+        }
+
+        impl::interop::InteropInputStream DataBuffer::GetInputStream() const
+        {
+            impl::interop::InteropInputStream stream = impl::interop::InteropInputStream(data.Get(), length);
+            stream.Position(position);
+
+            return stream;
+        }
+
+        DataBuffer DataBuffer::Clone() const
+        {
+            if (IsEmpty())
+                return DataBuffer();
+
+            impl::interop::SP_InteropMemory mem(new impl::interop::InteropUnpooledMemory(length));
+            mem.Get()->Length(length);
+            std::memcpy(mem.Get()->Data(), data.Get()->Data() + position, length);
+
+            return DataBuffer(mem, 0, length);
+        }
+
+        void DataBuffer::Skip(int32_t bytes)
+        {
+            int32_t toSkip = bytes < GetSize() ? bytes : GetSize();
+
+            Advance(toSkip);
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/error_handling_filter.cpp b/modules/platforms/cpp/network/src/network/error_handling_filter.cpp
new file mode 100644
index 0000000..fe14e40
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/error_handling_filter.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#include "network/error_handling_filter.h"
+
+#define CLOSE_CONNECTION_ON_EXCEPTION(...)                                  \
+    try                                                                     \
+    {                                                                       \
+        __VA_ARGS__;                                                        \
+    }                                                                       \
+    catch (const IgniteError& err)                                          \
+    {                                                                       \
+        DataFilterAdapter::Close(id, &err);                                 \
+    }                                                                       \
+    catch (std::exception& err)                                             \
+    {                                                                       \
+        std::string msg("Standard library exception is thrown: ");          \
+        msg += err.what();                                                  \
+        IgniteError err0(IgniteError::IGNITE_ERR_GENERIC, msg.c_str());     \
+        DataFilterAdapter::Close(id, &err0);                                \
+    }                                                                       \
+    catch (...)                                                             \
+    {                                                                       \
+        IgniteError err0(IgniteError::IGNITE_ERR_UNKNOWN,                   \
+            "Unknown error is encountered when processing network event");  \
+        DataFilterAdapter::Close(id, &err0);                                \
+    }
+
+namespace ignite
+{
+    namespace network
+    {
+        void ErrorHandlingFilter::OnConnectionSuccess(const EndPoint &addr, uint64_t id)
+        {
+            CLOSE_CONNECTION_ON_EXCEPTION(DataFilterAdapter::OnConnectionSuccess(addr, id))
+        }
+
+        void ErrorHandlingFilter::OnConnectionError(const EndPoint &addr, const IgniteError &err)
+        {
+            try
+            {
+                DataFilterAdapter::OnConnectionError(addr, err);
+            }
+            catch (...)
+            {
+                // No-op.
+            }
+        }
+
+        void ErrorHandlingFilter::OnConnectionClosed(uint64_t id, const IgniteError *err)
+        {
+            try
+            {
+                DataFilterAdapter::OnConnectionClosed(id, err);
+            }
+            catch (...)
+            {
+                // No-op.
+            }
+        }
+
+        void ErrorHandlingFilter::OnMessageReceived(uint64_t id, const DataBuffer &data)
+        {
+            CLOSE_CONNECTION_ON_EXCEPTION(DataFilterAdapter::OnMessageReceived(id, data))
+        }
+
+        void ErrorHandlingFilter::OnMessageSent(uint64_t id)
+        {
+            CLOSE_CONNECTION_ON_EXCEPTION(DataFilterAdapter::OnMessageSent(id))
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/error_handling_filter.h b/modules/platforms/cpp/network/src/network/error_handling_filter.h
new file mode 100644
index 0000000..5b74d77
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/error_handling_filter.h
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_ERROR_HANDLING_FILTER
+#define _IGNITE_NETWORK_ERROR_HANDLING_FILTER
+
+#include <ignite/network/data_filter_adapter.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        /**
+         * Filter that handles exceptions thrown by upper level handlers.
+         */
+        class IGNITE_IMPORT_EXPORT ErrorHandlingFilter : public DataFilterAdapter
+        {
+        public:
+            /**
+             * Destructor.
+             */
+            virtual ~ErrorHandlingFilter()
+            {
+                // No-op.
+            }
+
+            /**
+             * Callback that called on successful connection establishment.
+             *
+             * @param addr Address of the new connection.
+             * @param id Connection ID.
+             */
+            virtual void OnConnectionSuccess(const EndPoint& addr, uint64_t id);
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param addr Connection address.
+             * @param err Error.
+             */
+            virtual void OnConnectionError(const EndPoint& addr, const IgniteError& err);
+
+            /**
+             * Callback that called on error during connection establishment.
+             *
+             * @param id Async client ID.
+             * @param err Error. Can be null if connection closed without error.
+             */
+            virtual void OnConnectionClosed(uint64_t id, const IgniteError* err);
+
+            /**
+             * Callback that called when new message is received.
+             *
+             * @param id Async client ID.
+             * @param msg Received message.
+             */
+            virtual void OnMessageReceived(uint64_t id, const DataBuffer &msg);
+
+            /**
+             * Callback that called when message is sent.
+             *
+             * @param id Async client ID.
+             */
+            virtual void OnMessageSent(uint64_t id);
+        };
+    }
+}
+
+#endif //_IGNITE_NETWORK_ERROR_HANDLING_FILTER
\ No newline at end of file
diff --git a/modules/platforms/cpp/network/src/network/length_prefix_codec.cpp b/modules/platforms/cpp/network/src/network/length_prefix_codec.cpp
new file mode 100644
index 0000000..b1333fd
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/length_prefix_codec.cpp
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+#include <ignite/impl/binary/binary_utils.h>
+
+#include <ignite/network/length_prefix_codec.h>
+
+namespace ignite
+{
+    namespace network
+    {
+        using impl::interop::SP_InteropMemory;
+
+        LengthPrefixCodec::LengthPrefixCodec() :
+            packetSize(-1)
+        {
+            // No-op.
+        }
+
+        LengthPrefixCodec::~LengthPrefixCodec()
+        {
+            // No-op.
+        }
+
+        DataBuffer LengthPrefixCodec::Encode(DataBuffer& data)
+        {
+            // Just pass data as is, because we encode message size in
+            // the application to avoid unnecessary re-allocations and copying.
+            return data.ConsumeEntirely();
+        }
+
+        DataBuffer LengthPrefixCodec::Decode(DataBuffer& data)
+        {
+            if (packet.IsValid() && packet.Get()->Length() == (PACKET_HEADER_SIZE + packetSize))
+            {
+                packetSize = -1;
+                packet.Get()->Length(0);
+            }
+
+            if (packetSize < 0)
+            {
+                Consume(data, PACKET_HEADER_SIZE);
+
+                if (packet.Get()->Length() < PACKET_HEADER_SIZE)
+                    return DataBuffer();
+
+                packetSize = impl::binary::BinaryUtils::ReadInt32(*packet.Get(), 0);
+            }
+
+            Consume(data, PACKET_HEADER_SIZE + packetSize);
+
+            if (packet.Get()->Length() == (PACKET_HEADER_SIZE + packetSize))
+                return DataBuffer(packet, 0, PACKET_HEADER_SIZE + packetSize);
+
+            return DataBuffer();
+        }
+
+        void LengthPrefixCodec::Consume(DataBuffer &data, int32_t desired)
+        {
+            if (!packet.IsValid())
+                packet = impl::interop::SP_InteropMemory(new impl::interop::InteropUnpooledMemory(desired));
+
+            impl::interop::InteropMemory& packet0 = *packet.Get();
+
+            if (packet0.Capacity() < desired)
+                packet0.Reallocate(desired);
+
+            int32_t toCopy = desired - packet0.Length();
+            if (toCopy <= 0)
+                return;
+
+            if (data.GetSize() < toCopy)
+                toCopy = data.GetSize();
+
+            int8_t* dst = packet0.Data() + packet0.Length();
+            packet0.Length(packet0.Length() + toCopy);
+            data.Consume(dst, toCopy);
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/network.cpp b/modules/platforms/cpp/network/src/network/network.cpp
index ab1b295..6e10c17 100644
--- a/modules/platforms/cpp/network/src/network/network.cpp
+++ b/modules/platforms/cpp/network/src/network/network.cpp
@@ -17,8 +17,15 @@
 
 #include "network/sockets.h"
 
+#ifdef _WIN32
+#   include "network/win_async_client_pool.h"
+#else // Other. Assume Linux
+#   include "network/linux_async_client_pool.h"
+#endif
+
 #include <ignite/network/network.h>
 
+#include "network/async_client_pool_adapter.h"
 #include "network/ssl/ssl_gateway.h"
 #include "network/ssl/secure_socket_client.h"
 #include "network/tcp_socket_client.h"
@@ -34,18 +41,30 @@
                 SslGateway::GetInstance().LoadAll();
             }
 
-            IGNITE_IMPORT_EXPORT SocketClient* MakeTcpSocketClient()
-            {
-                return new TcpSocketClient;
-            }
-
-            IGNITE_IMPORT_EXPORT SocketClient* MakeSecureSocketClient(const std::string& certPath,
-                const std::string& keyPath, const std::string& caPath)
+            IGNITE_IMPORT_EXPORT SocketClient* MakeSecureSocketClient(const SecureConfiguration& cfg)
             {
                 EnsureSslLoaded();
 
-                return new SecureSocketClient(certPath, keyPath, caPath);
+                return new SecureSocketClient(cfg);
             }
         }
+
+        IGNITE_IMPORT_EXPORT SocketClient* MakeTcpSocketClient()
+        {
+            return new TcpSocketClient;
+        }
+
+        IGNITE_IMPORT_EXPORT SP_AsyncClientPool MakeAsyncClientPool(const std::vector<SP_DataFilter>& filters)
+        {
+            SP_AsyncClientPool platformPool = SP_AsyncClientPool(
+#ifdef _WIN32
+                new WinAsyncClientPool()
+#else // Other. Assume Linux
+                new LinuxAsyncClientPool()
+#endif
+            );
+
+            return SP_AsyncClientPool(new AsyncClientPoolAdapter(filters, platformPool));
+        }
     }
 }
diff --git a/modules/platforms/cpp/network/src/network/ssl/secure_data_filter.cpp b/modules/platforms/cpp/network/src/network/ssl/secure_data_filter.cpp
new file mode 100644
index 0000000..c1667f9
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/ssl/secure_data_filter.cpp
@@ -0,0 +1,296 @@
+/*
+ * 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.
+ */
+
+#include <memory>
+
+#include <ignite/common/utils.h>
+
+#include <ignite/network/network.h>
+#include <ignite/network/ssl/secure_data_filter.h>
+
+#include "network/ssl/ssl_gateway.h"
+#include "network/ssl/secure_utils.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        namespace ssl
+        {
+            SecureDataFilter::SecureDataFilter(const SecureConfiguration &cfg) :
+                sslContext(0),
+                contexts(0),
+                contextCs()
+            {
+                EnsureSslLoaded();
+
+                sslContext = MakeContext(cfg);
+
+                contexts = new ContextMap;
+            }
+
+            SecureDataFilter::~SecureDataFilter()
+            {
+                delete contexts;
+                FreeContext(static_cast<SSL_CTX*>(sslContext));
+            }
+
+            bool SecureDataFilter::Send(uint64_t id, const DataBuffer &data)
+            {
+                SP_SecureConnectionContext context = FindContext(id);
+                if (!context.IsValid())
+                    return false;
+
+                return context.Get()->Send(data);
+            }
+
+            void SecureDataFilter::OnConnectionSuccess(const EndPoint &addr, uint64_t id)
+            {
+                SP_SecureConnectionContext context(new SecureConnectionContext(id, addr, *this));
+
+                {
+                    common::concurrent::CsLockGuard lock(contextCs);
+
+                    contexts->insert(std::make_pair(id, context));
+                }
+
+                context.Get()->DoConnect();
+            }
+
+            void SecureDataFilter::OnConnectionClosed(uint64_t id, const IgniteError *err)
+            {
+                SP_SecureConnectionContext context = FindContext(id);
+                if (!context.IsValid())
+                    return;
+
+                if (context.Get()->IsConnected())
+                    DataFilterAdapter::OnConnectionClosed(id, err);
+                else
+                {
+                    std::stringstream sslErrMsg;
+                    sslErrMsg << "Connection closed during SSL/TLS handshake";
+                    if (err)
+                        sslErrMsg << ". Details: " << err->GetText();
+
+                    std::string sslErrMsgStr = sslErrMsg.str();
+
+                    IgniteError err0(IgniteError::IGNITE_ERR_SECURE_CONNECTION_FAILURE, sslErrMsgStr.c_str());
+
+                    DataFilterAdapter::OnConnectionError(context.Get()->GetAddress(), err0);
+                }
+
+                {
+                    common::concurrent::CsLockGuard lock(contextCs);
+
+                    contexts->erase(id);
+                }
+            }
+
+            void SecureDataFilter::OnMessageReceived(uint64_t id, const DataBuffer &msg)
+            {
+                SP_SecureConnectionContext context = FindContext(id);
+                if (!context.IsValid())
+                    return;
+
+                SecureConnectionContext& context0 = *context.Get();
+
+                DataBuffer in(msg);
+
+                while (!in.IsEmpty())
+                {
+                    bool connectionHappened = context0.ProcessData(in);
+
+                    if (connectionHappened)
+                        DataFilterAdapter::OnConnectionSuccess(context0.GetAddress(), id);
+
+                    if (context0.IsConnected())
+                    {
+                        DataBuffer data = context0.GetPendingDecryptedData();
+                        while (!data.IsEmpty())
+                        {
+                            DataFilterAdapter::OnMessageReceived(id, data);
+                            data = context0.GetPendingDecryptedData();
+                        }
+                    }
+                }
+            }
+
+            SecureDataFilter::SP_SecureConnectionContext SecureDataFilter::FindContext(uint64_t id)
+            {
+                common::concurrent::CsLockGuard lock(contextCs);
+
+                std::map<uint64_t, SP_SecureConnectionContext>::iterator it = contexts->find(id);
+                if (it == contexts->end())
+                    return SP_SecureConnectionContext();
+
+                return it->second;
+            }
+
+            bool SecureDataFilter::SendInternal(uint64_t id, const DataBuffer& data)
+            {
+                return DataFilterAdapter::Send(id, data);
+            }
+
+            SecureDataFilter::SecureConnectionContext::SecureConnectionContext(
+                uint64_t id,
+                const EndPoint &addr,
+                SecureDataFilter& filter
+            ) :
+                connected(false),
+                id(id),
+                addr(addr),
+                filter(filter),
+                ssl(0),
+                bioIn(0),
+                bioOut(0)
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                ssl = sslGateway.SSL_new_(static_cast<SSL_CTX*>(filter.sslContext));
+                if (!ssl)
+                    ThrowLastSecureError("Can not create secure connection");
+
+                bioIn = sslGateway.BIO_new_(sslGateway.BIO_s_mem_());
+                if (!bioIn)
+                    ThrowLastSecureError("Can not create input BIO");
+
+                bioOut = sslGateway.BIO_new_(sslGateway.BIO_s_mem_());
+                if (!bioOut)
+                    ThrowLastSecureError("Can not create output BIO");
+
+                sslGateway.SSL_set_bio_(static_cast<SSL*>(ssl), static_cast<BIO*>(bioIn), static_cast<BIO*>(bioOut));
+                sslGateway.SSL_set_connect_state_(static_cast<SSL*>(ssl));
+            }
+
+            SecureDataFilter::SecureConnectionContext::~SecureConnectionContext()
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                if (ssl)
+                    sslGateway.SSL_free_(static_cast<SSL*>(ssl));
+                else
+                {
+                    if (bioIn)
+                        sslGateway.BIO_free_all_(static_cast<BIO*>(bioIn));
+
+                    if (bioOut)
+                        sslGateway.BIO_free_all_(static_cast<BIO*>(bioOut));
+                }
+            }
+
+            bool SecureDataFilter::SecureConnectionContext::DoConnect()
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                SSL* ssl0 = static_cast<SSL*>(ssl);
+                int res = sslGateway.SSL_connect_(ssl0);
+
+                if (res != SSL_OPERATION_SUCCESS)
+                {
+                    int sslError = sslGateway.SSL_get_error_(ssl0, res);
+                    if (IsActualError(sslError))
+                        ThrowLastSecureError("Can not establish secure connection");
+                }
+
+                SendPendingData();
+
+                return res == SSL_OPERATION_SUCCESS;
+            }
+
+            bool SecureDataFilter::SecureConnectionContext::SendPendingData()
+            {
+                DataBuffer data(GetPendingData(bioOut));
+
+                if (data.IsEmpty())
+                    return false;
+
+                return filter.SendInternal(id, data);
+            }
+
+            bool SecureDataFilter::SecureConnectionContext::Send(const DataBuffer& data)
+            {
+                if (!connected)
+                    return false;
+
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                int res = sslGateway.SSL_write_(static_cast<SSL*>(ssl), data.GetData(), data.GetSize());
+                if (res <= 0)
+                    return false;
+
+                return SendPendingData();
+            }
+
+            bool SecureDataFilter::SecureConnectionContext::ProcessData(DataBuffer& data)
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+                int res = sslGateway.BIO_write_(static_cast<BIO*>(bioIn), data.GetData(), data.GetSize());
+                if (res <= 0)
+                    ThrowLastSecureError("Failed to process SSL data");
+
+                data.Skip(res);
+
+                SendPendingData();
+
+                if (connected)
+                    return false;
+
+                connected = DoConnect();
+
+                SendPendingData();
+
+                if (!connected)
+                    return false;
+
+                recvBuffer = impl::interop::SP_InteropMemory(
+                    new impl::interop::InteropUnpooledMemory(RECEIVE_BUFFER_SIZE));
+
+                return true;
+            }
+
+            DataBuffer SecureDataFilter::SecureConnectionContext::GetPendingData(void* bio)
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                BIO *bio0 = static_cast<BIO*>(bio);
+                int available = sslGateway.BIO_pending_(bio0);
+
+                impl::interop::SP_InteropMemory buf(new impl::interop::InteropUnpooledMemory(available));
+                buf.Get()->Length(available);
+
+                int res = sslGateway.BIO_read_(bio0, buf.Get()->Data(), buf.Get()->Length());
+                if (res <= 0)
+                    return DataBuffer();
+
+                return DataBuffer(buf);
+            }
+
+            DataBuffer SecureDataFilter::SecureConnectionContext::GetPendingDecryptedData()
+            {
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                SSL *ssl0 = static_cast<SSL*>(ssl);
+                int res = sslGateway.SSL_read_(ssl0, recvBuffer.Get()->Data(), recvBuffer.Get()->Capacity());
+                if (res <= 0)
+                    return DataBuffer();
+
+                recvBuffer.Get()->Length(res);
+                return DataBuffer(recvBuffer);
+            }
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.cpp b/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.cpp
index 9e4143d..d08244e 100644
--- a/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.cpp
+++ b/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.cpp
@@ -21,24 +21,13 @@
 #include <cassert>
 
 #include <ignite/common/utils.h>
-#include <ignite/common/concurrent.h>
-#include <ignite/ignite_error.h>
+#include <ignite/network/utils.h>
 
 #include "network/ssl/secure_socket_client.h"
+#include "network/ssl/secure_utils.h"
 #include "network/ssl/ssl_gateway.h"
 
-enum { OPERATION_SUCCESS = 1 };
-
-void FreeContext(SSL_CTX* ctx)
-{
-    using namespace ignite::network::ssl;
-
-    SslGateway &sslGateway = SslGateway::GetInstance();
-
-    assert(sslGateway.Loaded());
-
-    sslGateway.SSL_CTX_free_(ctx);
-}
+using namespace ignite::network::ssl;
 
 void FreeBio(BIO* bio)
 {
@@ -57,11 +46,8 @@
     {
         namespace ssl
         {
-            SecureSocketClient::SecureSocketClient(const std::string& certPath, const std::string& keyPath,
-                const std::string& caPath):
-                certPath(certPath),
-                keyPath(keyPath),
-                caPath(caPath),
+            SecureSocketClient::SecureSocketClient(const SecureConfiguration& cfg):
+                cfg(cfg),
                 context(0),
                 ssl(0),
                 blocking(true)
@@ -74,7 +60,7 @@
                 CloseInternal();
 
                 if (context)
-                    SslGateway::GetInstance().SSL_CTX_free_(reinterpret_cast<SSL_CTX*>(context));
+                    FreeContext(reinterpret_cast<SSL_CTX*>(context));
             }
 
             bool SecureSocketClient::Connect(const char* hostname, uint16_t port, int32_t timeout)
@@ -87,10 +73,10 @@
 
                 if (!context)
                 {
-                    context = MakeContext(certPath, keyPath, caPath);
+                    context = MakeContext(cfg);
 
                     if (!context)
-                        ThrowSecureError("Can not create SSL context. Aborting connect");
+                        ThrowLastSecureError("Can not create SSL context", "Aborting connect");
                 }
 
                 ssl = MakeSsl(context, hostname, port, blocking);
@@ -103,8 +89,8 @@
 
                 int res = sslGateway.SSL_set_tlsext_host_name_(ssl0, hostname);
 
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set host name for secure connection: " + GetSslError(ssl0, res));
+                if (res != SSL_OPERATION_SUCCESS)
+                    ThrowLastSecureError("Can not set host name for secure connection");
 
                 sslGateway.SSL_set_connect_state_(ssl0);
 
@@ -118,13 +104,21 @@
                 if (cert)
                     sslGateway.X509_free_(cert);
                 else
-                    ThrowSecureError("Remote host did not provide certificate: " + GetSslError(ssl0, res));
+                    ThrowLastSecureError("Remote host did not provide certificate");
 
-                // Verify the result of chain verification
-                // Verification performed according to RFC 4158
+                // Verify the result of chain verification.
+                // Verification performed according to RFC 4158.
                 res = sslGateway.SSL_get_verify_result_(ssl0);
                 if (X509_V_OK != res)
-                    ThrowSecureError("Certificate chain verification failed: " + GetSslError(ssl0, res));
+                    ThrowLastSecureError("Certificate chain verification failed");
+
+                res = WaitOnSocket(ssl, timeout, false);
+
+                if (res == WaitResult::TIMEOUT)
+                    return false;
+
+                if (res != WaitResult::SUCCESS)
+                    ThrowLastSecureError("Error while establishing secure connection");
 
                 guard.Release();
 
@@ -143,7 +137,7 @@
                 assert(sslGateway.Loaded());
 
                 if (!ssl)
-                    ThrowNetworkError("Trying to send data using closed connection");
+                    utils::ThrowNetworkError("Trying to send data using closed connection");
 
                 SSL* ssl0 = reinterpret_cast<SSL*>(ssl);
 
@@ -157,7 +151,7 @@
                 assert(sslGateway.Loaded());
 
                 if (!ssl)
-                    ThrowNetworkError("Trying to receive data using closed connection");
+                    utils::ThrowNetworkError("Trying to receive data using closed connection");
 
                 SSL* ssl0 = reinterpret_cast<SSL*>(ssl);
 
@@ -197,53 +191,6 @@
                 return blocking;
             }
 
-            void* SecureSocketClient::MakeContext(const std::string& certPath, const std::string& keyPath,
-                const std::string& caPath)
-            {
-                SslGateway &sslGateway = SslGateway::GetInstance();
-
-                assert(sslGateway.Loaded());
-
-                const SSL_METHOD* method = sslGateway.SSLv23_client_method_();
-                if (!method)
-                    ThrowSecureError("Can not get SSL method");
-
-                SSL_CTX* ctx = sslGateway.SSL_CTX_new_(method);
-                if (!ctx)
-                    ThrowSecureError("Can not create new SSL context");
-
-                common::DeinitGuard<SSL_CTX> guard(ctx, &FreeContext);
-
-                sslGateway.SSL_CTX_set_verify_(ctx, SSL_VERIFY_PEER, 0);
-
-                sslGateway.SSL_CTX_set_verify_depth_(ctx, 8);
-
-                sslGateway.SSL_CTX_set_options_(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION);
-
-                const char* cCaPath = caPath.empty() ? 0 : caPath.c_str();
-
-                long res = sslGateway.SSL_CTX_load_verify_locations_(ctx, cCaPath, 0);
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set Certificate Authority path for secure connection");
-
-                res = sslGateway.SSL_CTX_use_certificate_chain_file_(ctx, certPath.c_str());
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set client certificate file for secure connection");
-
-                res = sslGateway.SSL_CTX_use_RSAPrivateKey_file_(ctx, keyPath.c_str(), SSL_FILETYPE_PEM);
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set private key file for secure connection");
-
-                const char* const PREFERRED_CIPHERS = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
-                res = sslGateway.SSL_CTX_set_cipher_list_(ctx, PREFERRED_CIPHERS);
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set ciphers list for secure connection");
-
-                guard.Release();
-
-                return ctx;
-            }
-
             void* SecureSocketClient::MakeSsl(void* context, const char* hostname, uint16_t port, bool& blocking)
             {
                 SslGateway &sslGateway = SslGateway::GetInstance();
@@ -252,11 +199,11 @@
 
                 BIO* bio = sslGateway.BIO_new_ssl_connect_(reinterpret_cast<SSL_CTX*>(context));
                 if (!bio)
-                    ThrowSecureError("Can not create SSL connection");
+                    ThrowLastSecureError("Can not create SSL connection");
 
                 common::DeinitGuard<BIO> guard(bio, &FreeBio);
 
-                blocking = sslGateway.BIO_set_nbio_(bio, 1) != OPERATION_SUCCESS;
+                blocking = sslGateway.BIO_set_nbio_(bio, 1) != SSL_OPERATION_SUCCESS;
 
                 std::stringstream stream;
                 stream << hostname << ":" << port;
@@ -264,13 +211,13 @@
                 std::string address = stream.str();
 
                 long res = sslGateway.BIO_set_conn_hostname_(bio, address.c_str());
-                if (res != OPERATION_SUCCESS)
-                    ThrowSecureError("Can not set SSL connection hostname");
+                if (res != SSL_OPERATION_SUCCESS)
+                    ThrowLastSecureError("Can not set SSL connection hostname");
 
                 SSL* ssl = 0;
                 sslGateway.BIO_get_ssl_(bio, &ssl);
                 if (!ssl)
-                    ThrowSecureError("Can not get SSL instance from BIO");
+                    ThrowLastSecureError("Can not get SSL instance from BIO");
 
                 guard.Release();
 
@@ -289,13 +236,13 @@
                 {
                     int res = sslGateway.SSL_connect_(ssl0);
 
-                    if (res == OPERATION_SUCCESS)
+                    if (res == SSL_OPERATION_SUCCESS)
                         return true;
 
                     int sslError = sslGateway.SSL_get_error_(ssl0, res);
 
                     if (IsActualError(sslError))
-                        ThrowSecureError("Can not establish secure connection: " + GetSslError(ssl0, res));
+                        ThrowLastSecureError("Can not establish secure connection");
 
                     int want = sslGateway.SSL_want_(ssl0);
 
@@ -305,58 +252,7 @@
                         return false;
 
                     if (res != WaitResult::SUCCESS)
-                        ThrowSecureError("Error while establishing secure connection: " + GetSslError(ssl0, res));
-                }
-            }
-
-            std::string SecureSocketClient::GetSslError(void* ssl, int ret)
-            {
-                SslGateway &sslGateway = SslGateway::GetInstance();
-
-                assert(sslGateway.Loaded());
-
-                SSL* ssl0 = reinterpret_cast<SSL*>(ssl);
-
-                int sslError = sslGateway.SSL_get_error_(ssl0, ret);
-
-                switch (sslError)
-                {
-                    case SSL_ERROR_NONE:
-                        break;
-
-                    case SSL_ERROR_WANT_WRITE:
-                        return std::string("SSL_connect wants write");
-
-                    case SSL_ERROR_WANT_READ:
-                        return std::string("SSL_connect wants read");
-
-                    default:
-                        return std::string("SSL error: ") + common::LexicalCast<std::string>(sslError);
-                }
-
-                long error = sslGateway.ERR_get_error_();
-
-                char errBuf[1024] = { 0 };
-
-                sslGateway.ERR_error_string_n_(error, errBuf, sizeof(errBuf));
-
-                return std::string(errBuf);
-            }
-
-            bool SecureSocketClient::IsActualError(int err)
-            {
-                switch (err)
-                {
-                    case SSL_ERROR_NONE:
-                    case SSL_ERROR_WANT_WRITE:
-                    case SSL_ERROR_WANT_READ:
-                    case SSL_ERROR_WANT_CONNECT:
-                    case SSL_ERROR_WANT_ACCEPT:
-                    case SSL_ERROR_WANT_X509_LOOKUP:
-                        return false;
-
-                    default:
-                        return true;
+                        ThrowLastSecureError("Error while establishing secure connection");
                 }
             }
 
@@ -372,27 +268,21 @@
                 }
             }
 
-            void SecureSocketClient::ThrowSecureError(const std::string& err)
-            {
-                throw IgniteError(IgniteError::IGNITE_ERR_SECURE_CONNECTION_FAILURE, err.c_str());
-            }
-
             int SecureSocketClient::WaitOnSocket(void* ssl, int32_t timeout, bool rd)
             {
                 SslGateway &sslGateway = SslGateway::GetInstance();
 
                 assert(sslGateway.Loaded());
-                
+
                 SSL* ssl0 = reinterpret_cast<SSL*>(ssl);
                 int fd = sslGateway.SSL_get_fd_(ssl0);
 
                 if (fd < 0)
                 {
                     std::stringstream ss;
+                    ss << "Can not get file descriptor from the SSL socket, fd=" << fd;
 
-                    ss << "Can not get file descriptor from the SSL socket: " << fd << ", " << GetSslError(ssl, fd);
-
-                    ThrowNetworkError(ss.str());
+                    ThrowLastSecureError(ss.str());
                 }
 
                 return sockets::WaitOnSocket(fd, timeout, rd);
diff --git a/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.h b/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.h
index 76d0392..cbf180f 100644
--- a/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.h
+++ b/modules/platforms/cpp/network/src/network/ssl/secure_socket_client.h
@@ -22,6 +22,7 @@
 #include <string>
 
 #include <ignite/network/socket_client.h>
+#include <ignite/network/ssl/secure_configuration.h>
 
 namespace ignite
 {
@@ -38,11 +39,9 @@
                 /**
                  * Constructor.
                  *
-                 * @param certPath Certificate file path.
-                 * @param keyPath Private key file path.
-                 * @param caPath Certificate authority file path.
+                 * @param cfg Secure configuration.
                  */
-                SecureSocketClient(const std::string& certPath, const std::string& keyPath, const std::string& caPath);
+                SecureSocketClient(const SecureConfiguration& cfg);
 
                 /**
                  * Destructor.
@@ -99,13 +98,6 @@
                 void CloseInternal();
 
                 /**
-                 * Throw SSL-related error.
-                 *
-                 * @param err Error message.
-                 */
-                static void ThrowSecureError(const std::string& err);
-
-                /**
                  * Wait on the socket for any event for specified time.
                  * This function uses poll to achive timeout functionality
                  * for every separate socket operation.
@@ -119,15 +111,14 @@
                 static int WaitOnSocket(void* ssl, int32_t timeout, bool rd);
 
                 /**
-                 * Make new context instance.
+                 * Wait on the socket if it's required by SSL.
                  *
-                 * @param certPath Certificate file path.
-                 * @param keyPath Private key file path.
-                 * @param caPath Certificate authority file path.
-                 * @return New context instance on success and null-pointer on fail.
+                 * @param res Operation result.
+                 * @param ssl SSl instance.
+                 * @param timeout Timeout in seconds.
+                 * @return
                  */
-                static void* MakeContext(const std::string& certPath, const std::string& keyPath,
-                    const std::string& caPath);
+                static int WaitOnSocketIfNeeded(int res, void* ssl, int timeout);
 
                 /**
                  * Make new SSL instance.
@@ -149,31 +140,8 @@
                  */
                 static bool CompleteConnectInternal(void* ssl, int timeout);
 
-                /**
-                 * Get SSL error.
-                 *
-                 * @param ssl SSL instance.
-                 * @param ret Return value of the pervious operation.
-                 * @return Error string.
-                 */
-                static std::string GetSslError(void* ssl, int ret);
-
-                /**
-                 * Check if a actual error occured.
-                 *
-                 * @param err SSL error code.
-                 * @return @true if a actual error occured
-                 */
-                static bool IsActualError(int err);
-
-                /** Certificate file path. */
-                std::string certPath;
-
-                /** Private key file path. */
-                std::string keyPath;
-
-                /** Certificate authority file path. */
-                std::string caPath;
+                /** Secure configuration. */
+                SecureConfiguration cfg;
 
                 /** SSL context. */
                 void* context;
diff --git a/modules/platforms/cpp/network/src/network/ssl/secure_utils.cpp b/modules/platforms/cpp/network/src/network/ssl/secure_utils.cpp
new file mode 100644
index 0000000..24c891e
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/ssl/secure_utils.cpp
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+
+#ifdef _WIN32
+#include "network/sockets.h"
+#include <wincrypt.h>
+#endif
+
+#include <ignite/ignite_error.h>
+
+#include <ignite/common/utils.h>
+#include <ignite/network/network.h>
+
+#include "network/ssl/secure_utils.h"
+
+namespace
+{
+    void LoadDefaultCa(SSL_CTX* sslContext)
+    {
+        using namespace ignite::common;
+        using namespace ignite::network::ssl;
+
+        assert(sslContext != 0);
+        SslGateway &sslGateway = SslGateway::GetInstance();
+
+#ifndef _WIN32
+        long res = sslGateway.SSL_CTX_set_default_verify_paths_(sslContext);
+        if (res != SSL_OPERATION_SUCCESS)
+            ThrowLastSecureError("Can not set default Certificate Authority for secure connection",
+                 "Try setting custom CA");
+#else
+        X509_STORE *sslStore = sslGateway.X509_STORE_new_();
+        if (!sslStore)
+            ThrowLastSecureError("Can not create X509_STORE certificate store", "Try setting custom CA");
+
+        HCERTSTORE sysStore = CertOpenSystemStoreA(NULL, "ROOT");
+        if (!sysStore)
+            ThrowSecureError(GetLastSystemError("Can not open System Certificate store for secure connection",
+                "Try setting custom CA"));
+
+        PCCERT_CONTEXT certIter = CertEnumCertificatesInStore(sysStore, NULL);
+        while (certIter)
+        {
+            const unsigned char *currentCert = certIter->pbCertEncoded;
+            X509* x509 = sslGateway.d2i_X509_(NULL, &currentCert, static_cast<long>(certIter->cbCertEncoded));
+            if (x509)
+            {
+                sslGateway.X509_STORE_add_cert_(sslStore, x509);
+
+                sslGateway.X509_free_(x509);
+            }
+            certIter = CertEnumCertificatesInStore(sysStore, certIter);
+        }
+
+        CertCloseStore(sysStore, 0);
+
+        sslGateway.SSL_CTX_set_cert_store_(sslContext, sslStore);
+#endif
+    }
+}
+
+namespace ignite
+{
+    namespace network
+    {
+        namespace ssl
+        {
+            SSL_CTX* MakeContext(const SecureConfiguration& cfg)
+            {
+                EnsureSslLoaded();
+
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                const SSL_METHOD* method = sslGateway.SSLv23_client_method_();
+                if (!method)
+                    ThrowLastSecureError("Can not get SSL method");
+
+                SSL_CTX* sslContext = sslGateway.SSL_CTX_new_(method);
+                if (!sslContext)
+                    ThrowLastSecureError("Can not create new SSL context");
+
+                common::DeinitGuard<SSL_CTX> guard(sslContext, &FreeContext);
+
+                sslGateway.SSL_CTX_set_verify_(sslContext, SSL_VERIFY_PEER, 0);
+
+                sslGateway.SSL_CTX_set_verify_depth_(sslContext, 8);
+
+                sslGateway.SSL_CTX_set_options_(sslContext, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION);
+
+                if (!cfg.caPath.empty())
+                {
+                    long res = sslGateway.SSL_CTX_load_verify_locations_(sslContext, cfg.caPath.c_str(), 0);
+                    if (res != SSL_OPERATION_SUCCESS)
+                        ThrowLastSecureError("Can not set Certificate Authority path for secure connection, path=" +
+                            cfg.caPath);
+                }
+                else
+                    LoadDefaultCa(sslContext);
+
+                if (!cfg.certPath.empty())
+                {
+                    long res = sslGateway.SSL_CTX_use_certificate_chain_file_(sslContext, cfg.certPath.c_str());
+                    if (res != SSL_OPERATION_SUCCESS)
+                        ThrowLastSecureError("Can not set client certificate file for secure connection, path=" +
+                            cfg.certPath);
+                }
+
+                if (!cfg.keyPath.empty())
+                {
+                    long res = sslGateway.SSL_CTX_use_RSAPrivateKey_file_(sslContext, cfg.keyPath.c_str(), SSL_FILETYPE_PEM);
+                    if (res != SSL_OPERATION_SUCCESS)
+                        ThrowLastSecureError("Can not set private key file for secure connection, path=" + cfg.keyPath);
+                }
+
+                const char* const PREFERRED_CIPHERS = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
+                long res = sslGateway.SSL_CTX_set_cipher_list_(sslContext, PREFERRED_CIPHERS);
+                if (res != SSL_OPERATION_SUCCESS)
+                    ThrowLastSecureError("Can not set ciphers list for secure connection");
+
+                guard.Release();
+                return sslContext;
+            }
+
+            void FreeContext(SSL_CTX* ctx)
+            {
+                using namespace ignite::network::ssl;
+
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                assert(sslGateway.Loaded());
+
+                sslGateway.SSL_CTX_free_(ctx);
+            }
+
+            bool IsActualError(int err)
+            {
+                switch (err)
+                {
+                    case SSL_ERROR_NONE:
+                    case SSL_ERROR_WANT_READ:
+                    case SSL_ERROR_WANT_WRITE:
+                    case SSL_ERROR_WANT_X509_LOOKUP:
+                    case SSL_ERROR_WANT_CONNECT:
+                    case SSL_ERROR_WANT_ACCEPT:
+                        return false;
+
+                    default:
+                        return true;
+                }
+            }
+
+            void ThrowSecureError(const std::string& err)
+            {
+                throw IgniteError(IgniteError::IGNITE_ERR_SECURE_CONNECTION_FAILURE, err.c_str());
+            }
+
+            std::string GetLastSecureError()
+            {
+                using namespace ignite::network::ssl;
+
+                SslGateway &sslGateway = SslGateway::GetInstance();
+
+                assert(sslGateway.Loaded());
+
+                unsigned long errorCode = sslGateway.ERR_get_error_();
+
+                std::string errorDetails;
+                if (errorCode != 0)
+                {
+                    char errBuf[1024] = { 0 };
+
+                    sslGateway.ERR_error_string_n_(errorCode, errBuf, sizeof(errBuf));
+
+                    errorDetails.assign(errBuf);
+                }
+
+                return errorDetails;
+            }
+
+            void ThrowLastSecureError(const std::string& description, const std::string& advice)
+            {
+                ThrowSecureError(common::FormatErrorMessage(description, GetLastSecureError(), advice));
+            }
+
+            void ThrowLastSecureError(const std::string& description)
+            {
+                std::string empty;
+                ThrowLastSecureError(description, empty);
+            }
+        }
+    }
+}
diff --git a/modules/platforms/cpp/network/src/network/ssl/secure_utils.h b/modules/platforms/cpp/network/src/network/ssl/secure_utils.h
new file mode 100644
index 0000000..cc866ad
--- /dev/null
+++ b/modules/platforms/cpp/network/src/network/ssl/secure_utils.h
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_NETWORK_SSL_SECURE_UTILS
+#define _IGNITE_NETWORK_SSL_SECURE_UTILS
+
+#include <ignite/network/ssl/secure_configuration.h>
+#include "network/ssl/ssl_gateway.h"
+
+namespace ignite
+{
+    namespace network
+    {
+        namespace ssl
+        {
+            enum
+            {
+                /** OpenSSL functions return this code on success. */
+                SSL_OPERATION_SUCCESS = 1,
+            };
+
+            /**
+             * Make SSL context using configuration.
+             *
+             * @param cfg Configuration to use.
+             * @return New context instance on success.
+             * @throw IgniteError on error.
+             */
+            SSL_CTX* MakeContext(const SecureConfiguration& cfg);
+
+            /**
+             * Free context.
+             *
+             * @param ctx Context to free.
+             */
+            void FreeContext(SSL_CTX* ctx);
+
+            /**
+             * Check whether error is actual error or code returned when used in async environment.
+             *
+             * @param err Error obtained with SSL_get_error.
+             * @return @c true if the code returned on actual error.
+             */
+            bool IsActualError(int err);
+
+            /**
+             * Throw SSL-related error.
+             *
+             * @param err Error message.
+             */
+            void ThrowSecureError(const std::string& err);
+
+            /**
+             * Get SSL-related error in text format.
+             *
+             * @param err Error message in human-readable format.
+             */
+            std::string GetLastSecureError();
+
+            /**
+             * Try extract from OpenSSL error stack and throw SSL-related error.
+             *
+             * @param description Error description.
+             * @param advice User advice.
+             */
+            void ThrowLastSecureError(const std::string& description, const std::string& advice);
+
+            /**
+             * Try extract from OpenSSL error stack and throw SSL-related error.
+             *
+             * @param description Error description.
+             */
+            void ThrowLastSecureError(const std::string& description);
+        }
+    }
+}
+
+#endif //_IGNITE_NETWORK_SSL_SECURE_UTILS
diff --git a/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.cpp b/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.cpp
index b2afb0f..9a8e449 100644
--- a/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.cpp
+++ b/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.cpp
@@ -66,34 +66,25 @@
                 memset(&functions, 0, sizeof(functions));
             }
 
-            common::dynamic::Module SslGateway::LoadSslLibrary(const char* name)
+            common::dynamic::Module SslGateway::LoadSslLibrary(const std::string& name, const std::string& homeDir)
             {
                 using namespace common;
                 using namespace dynamic;
 
-                std::string home = GetEnv(ADDITIONAL_OPENSSL_HOME_ENV);
-
-                if (home.empty())
-                    home = GetEnv("OPENSSL_HOME");
-
                 std::string fullName = GetDynamicLibraryName(name);
 
-                if (!home.empty())
+                if (!homeDir.empty())
                 {
-                    const char* paths[] = {"bin", "lib"};
+#ifdef _WIN32
+                    const char* binSubDir = "bin";
+#else
+                    const char* binSubDir = "lib";
+#endif
+                    std::ostringstream oss;
 
-                    for (size_t i = 0; i < 2; i++) {
-                        std::stringstream constructor;
+                    oss << homeDir << Fs << binSubDir << Fs << fullName;
 
-                        constructor << home << Fs << paths[i] << Fs << fullName;
-
-                        std::string fullPath = constructor.str();
-
-                        Module mod = LoadModule(fullPath);
-
-                        if (mod.IsLoaded())
-                            return mod;
-                    }
+                    return LoadModule(oss.str());
                 }
 
                 return LoadModule(fullName);
@@ -101,42 +92,87 @@
 
             void SslGateway::LoadSslLibraries()
             {
-                libssl = LoadSslLibrary("libssl");
+                using namespace common;
 
-                if (!libssl.IsLoaded())
+                std::string home = GetEnv(ADDITIONAL_OPENSSL_HOME_ENV);
+                if (home.empty())
+                    home = GetEnv("OPENSSL_HOME");
+
+                bool isLoaded = false;
+
+                if (!home.empty())
+                    isLoaded = TryLoadSslLibraries(home);
+
+                // Try load from system path.
+                if (!isLoaded)
+                    isLoaded = TryLoadSslLibraries("");
+
+                if (!isLoaded)
                 {
-                    libcrypto = LoadSslLibrary("libcrypto-1_1-x64");
-                    libssl = LoadSslLibrary("libssl-1_1-x64");
-                }
-
-                if (!libssl.IsLoaded())
-                {
-                    libeay32 = LoadSslLibrary("libeay32");
-                    ssleay32 = LoadSslLibrary("ssleay32");
-                }
-
-                if (!libssl.IsLoaded() && (!libeay32.IsLoaded() || !ssleay32.IsLoaded()))
-                {
-                    if (!libssl.IsLoaded())
-                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                            "Can not load neccessary OpenSSL library: libssl");
-
+#ifdef _WIN32
                     std::stringstream ss;
 
-                    ss << "Can not load neccessary OpenSSL libraries:";
+                    ss << "Can not load necessary OpenSSL libraries:";
 
-                    if (!libeay32.IsLoaded())
-                        ss << " libeay32";
+                    if (!libssl.IsLoaded() || !libcrypto.IsLoaded())
+                    {
+                        if (!libssl.IsLoaded())
+                            ss << " libssl";
 
-                    if (!ssleay32.IsLoaded())
-                        ss << " ssleay32";
+                        if (!libcrypto.IsLoaded())
+                            ss << " libcrypto";
+                    }
+                    else
+                    {
+                        if (!libeay32.IsLoaded())
+                            ss << " libeay32";
+
+                        if (!ssleay32.IsLoaded())
+                            ss << " ssleay32";
+                    }
 
                     std::string res = ss.str();
 
                     throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, res.c_str());
+#else
+                    if (!libssl.IsLoaded())
+                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
+                            "Can not load necessary OpenSSL library: libssl");
+#endif
                 }
             }
 
+            bool SslGateway::TryLoadSslLibraries(const std::string& homeDir)
+            {
+#ifdef _WIN32
+#ifdef _WIN64
+#define SSL_LIB_PLATFORM_POSTFIX "-x64"
+#else
+#define SSL_LIB_PLATFORM_POSTFIX ""
+#endif
+                libcrypto = LoadSslLibrary("libcrypto-3" SSL_LIB_PLATFORM_POSTFIX, homeDir);
+                libssl = LoadSslLibrary("libssl-3" SSL_LIB_PLATFORM_POSTFIX, homeDir);
+
+                if (!libssl.IsLoaded() || !libcrypto.IsLoaded())
+                {
+                    libcrypto = LoadSslLibrary("libcrypto-1_1" SSL_LIB_PLATFORM_POSTFIX, homeDir);
+                    libssl = LoadSslLibrary("libssl-1_1" SSL_LIB_PLATFORM_POSTFIX, homeDir);
+                }
+
+                if (!libssl.IsLoaded() || !libcrypto.IsLoaded())
+                {
+                    libeay32 = LoadSslLibrary("libeay32", homeDir);
+                    ssleay32 = LoadSslLibrary("ssleay32", homeDir);
+                }
+
+                return (libssl.IsLoaded() && libcrypto.IsLoaded()) || (libeay32.IsLoaded() && ssleay32.IsLoaded());
+#else
+                libssl = LoadSslLibrary("libssl", homeDir);
+
+                return libssl.IsLoaded();
+#endif
+            }
+
             void SslGateway::LoadMandatoryMethods()
             {
                 functions.fpSSLeay_version = TryLoadSslMethod("SSLeay_version");
@@ -159,6 +195,8 @@
                 functions.fpSSL_CTX_free = LoadSslMethod("SSL_CTX_free");
                 functions.fpSSL_CTX_set_verify = LoadSslMethod("SSL_CTX_set_verify");
                 functions.fpSSL_CTX_set_verify_depth = LoadSslMethod("SSL_CTX_set_verify_depth");
+                functions.fpSSL_CTX_set_cert_store = LoadSslMethod("SSL_CTX_set_cert_store");
+                functions.fpSSL_CTX_set_default_verify_paths = LoadSslMethod("SSL_CTX_set_default_verify_paths");
                 functions.fpSSL_CTX_load_verify_locations = LoadSslMethod("SSL_CTX_load_verify_locations");
                 functions.fpSSL_CTX_use_certificate_chain_file = LoadSslMethod("SSL_CTX_use_certificate_chain_file");
                 functions.fpSSL_CTX_use_RSAPrivateKey_file = LoadSslMethod("SSL_CTX_use_RSAPrivateKey_file");
@@ -166,22 +204,38 @@
 
                 functions.fpSSL_get_verify_result = LoadSslMethod("SSL_get_verify_result");
 
-                functions.fpSSL_get_peer_certificate = LoadSslMethod("SSL_get_peer_certificate");
+                functions.fpSSL_get_peer_certificate = TryLoadSslMethod("SSL_get_peer_certificate");
+                // OpenSSL >= 3.0.0
+                if (!functions.fpSSL_get_peer_certificate)
+                    functions.fpSSL_get_peer_certificate = LoadSslMethod("SSL_get1_peer_certificate");
+
                 functions.fpSSL_ctrl = LoadSslMethod("SSL_ctrl");
                 functions.fpSSL_CTX_ctrl = LoadSslMethod("SSL_CTX_ctrl");
 
                 functions.fpSSL_set_connect_state = LoadSslMethod("SSL_set_connect_state");
                 functions.fpSSL_connect = LoadSslMethod("SSL_connect");
+                functions.fpSSL_set_bio = LoadSslMethod("SSL_set_bio");
                 functions.fpSSL_get_error = LoadSslMethod("SSL_get_error");
                 functions.fpSSL_want = LoadSslMethod("SSL_want");
                 functions.fpSSL_write = LoadSslMethod("SSL_write");
                 functions.fpSSL_read = LoadSslMethod("SSL_read");
                 functions.fpSSL_pending = LoadSslMethod("SSL_pending");
+                functions.fpSSL_get_version = LoadSslMethod("SSL_get_version");
+
                 functions.fpSSL_get_fd = LoadSslMethod("SSL_get_fd");
+                functions.fpSSL_new = LoadSslMethod("SSL_new");
                 functions.fpSSL_free = LoadSslMethod("SSL_free");
+                functions.fpBIO_new = LoadSslMethod("BIO_new");
                 functions.fpBIO_new_ssl_connect = LoadSslMethod("BIO_new_ssl_connect");
+                functions.fpBIO_s_mem = LoadSslMethod("BIO_s_mem");
+                functions.fpBIO_read = LoadSslMethod("BIO_read");
+                functions.fpBIO_write = LoadSslMethod("BIO_write");
+
 
                 functions.fpOPENSSL_config = LoadSslMethod("OPENSSL_config");
+                functions.fpX509_STORE_new = LoadSslMethod("X509_STORE_new");
+                functions.fpX509_STORE_add_cert = LoadSslMethod("X509_STORE_add_cert");
+                functions.fpd2i_X509 = LoadSslMethod("d2i_X509");
                 functions.fpX509_free = LoadSslMethod("X509_free");
 
                 functions.fpBIO_free_all = LoadSslMethod("BIO_free_all");
@@ -217,7 +271,6 @@
                 LoadMandatoryMethods();
 
                 functions.fpSSL_CTX_set_options = TryLoadSslMethod("SSL_CTX_set_options");
-                functions.fpERR_print_errors_fp = TryLoadSslMethod("ERR_print_errors_fp");
 
                 IGNITE_UNUSED(SSL_library_init_());
 
@@ -264,7 +317,7 @@
                 return fp;
             }
 
-            char* SslGateway::SSLeay_version_(int type)
+            char* SslGateway::OpenSSL_version_(int type)
             {
                 typedef char* (FuncType)(int);
 
@@ -360,6 +413,28 @@
                 fp(ctx, depth);
             }
 
+            void SslGateway::SSL_CTX_set_cert_store_(SSL_CTX* ctx, X509_STORE* store)
+            {
+                assert(functions.fpSSL_CTX_set_cert_store != 0);
+
+                typedef int (FuncType)(SSL_CTX*, X509_STORE*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpSSL_CTX_set_cert_store);
+
+                fp(ctx, store);
+            }
+
+            int SslGateway::SSL_CTX_set_default_verify_paths_(SSL_CTX* ctx)
+            {
+                assert(functions.fpSSL_CTX_set_default_verify_paths != 0);
+
+                typedef int (FuncType)(SSL_CTX*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpSSL_CTX_set_default_verify_paths);
+
+                return fp(ctx);
+            }
+
             int SslGateway::SSL_CTX_load_verify_locations_(SSL_CTX* ctx, const char* cAfile, const char* cApath)
             {
                 assert(functions.fpSSL_CTX_load_verify_locations != 0);
@@ -495,6 +570,17 @@
                 return fp(s);
             }
 
+            void SslGateway::SSL_set_bio_(SSL* s, BIO* rbio, BIO* wbio)
+            {
+                assert(functions.fpSSL_set_bio != 0);
+
+                typedef void (FuncType)(SSL*, BIO*, BIO*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpSSL_set_bio);
+
+                fp(s, rbio, wbio);
+            }
+
             int SslGateway::SSL_get_error_(const SSL* s, int ret)
             {
                 assert(functions.fpSSL_get_error != 0);
@@ -550,6 +636,17 @@
                 return fp(ssl);
             }
 
+            const char* SslGateway::SSL_get_version_(const SSL* ssl)
+            {
+                assert(functions.fpSSL_get_version != 0);
+
+                typedef const char*(FuncType)(const SSL*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpSSL_get_version);
+
+                return fp(ssl);
+            }
+
             int SslGateway::SSL_get_fd_(const SSL* ssl)
             {
                 assert(functions.fpSSL_get_fd != 0);
@@ -561,6 +658,17 @@
                 return fp(ssl);
             }
 
+            SSL* SslGateway::SSL_new_(SSL_CTX* ctx)
+            {
+                assert(functions.fpSSL_new != 0);
+
+                typedef SSL* (FuncType)(SSL_CTX*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpSSL_new);
+
+                return fp(ctx);
+            }
+
             void SslGateway::SSL_free_(SSL* ssl)
             {
                 assert(functions.fpSSL_free != 0);
@@ -608,15 +716,59 @@
                 fp(configName);
             }
 
-            void SslGateway::X509_free_(X509* a)
+            X509_STORE* SslGateway::X509_STORE_new_()
+            {
+                assert(functions.fpX509_STORE_new != 0);
+
+                typedef X509_STORE*(FuncType)();
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpX509_STORE_new);
+
+                return fp();
+            }
+
+            int SslGateway::X509_STORE_add_cert_(X509_STORE* ctx, X509* cert)
+            {
+                assert(functions.fpX509_STORE_add_cert != 0);
+
+                typedef int(FuncType)(X509_STORE*, X509*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpX509_STORE_add_cert);
+
+                return fp(ctx, cert);
+            }
+
+            X509* SslGateway::d2i_X509_(X509** cert, const unsigned char** ppin, long length)
+            {
+                assert(functions.fpd2i_X509 != 0);
+
+                typedef X509*(FuncType)(X509**, const unsigned char**, long);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpd2i_X509);
+
+                return fp(cert, ppin, length);
+            }
+
+            void SslGateway::X509_free_(X509* cert)
             {
                 assert(functions.fpX509_free != 0);
 
-                typedef void (FuncType)(X509*);
+                typedef void(FuncType)(X509*);
 
                 FuncType* fp = reinterpret_cast<FuncType*>(functions.fpX509_free);
 
-                fp(a);
+                fp(cert);
+            }
+
+            BIO* SslGateway::BIO_new_(const BIO_METHOD* method)
+            {
+                assert(functions.fpBIO_new != 0);
+
+                typedef BIO*(FuncType)(const BIO_METHOD*);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpBIO_new);
+
+                return fp(method);
             }
 
             BIO* SslGateway::BIO_new_ssl_connect_(SSL_CTX* ctx)
@@ -641,6 +793,44 @@
                 fp(a);
             }
 
+            const BIO_METHOD* SslGateway::BIO_s_mem_()
+            {
+                assert(functions.fpBIO_s_mem != 0);
+
+                typedef const BIO_METHOD* (FuncType)();
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpBIO_s_mem);
+
+                return fp();
+            }
+
+            int SslGateway::BIO_read_(BIO* b, void* data, int len)
+            {
+                assert(functions.fpBIO_read != 0);
+
+                typedef int (FuncType)(BIO*, void*, int);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpBIO_read);
+
+                return fp(b, data, len);
+            }
+
+            int SslGateway::BIO_write_(BIO* b, const void *data, int len)
+            {
+                assert(functions.fpBIO_write != 0);
+
+                typedef int (FuncType)(BIO*, const void*, int);
+
+                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpBIO_write);
+
+                return fp(b, data, len);
+            }
+
+            int SslGateway::BIO_pending_(BIO* b)
+            {
+                return BIO_ctrl_(b, BIO_CTRL_PENDING, 0, NULL);
+            }
+
             long SslGateway::BIO_ctrl_(BIO* bp, int cmd, long larg, void* parg)
             {
                 assert(functions.fpBIO_ctrl != 0);
@@ -652,11 +842,6 @@
                 return fp(bp, cmd, larg, parg);
             }
 
-            long SslGateway::BIO_get_fd_(BIO* bp, int* fd)
-            {
-                return BIO_ctrl_(bp, BIO_C_GET_FD, 0, reinterpret_cast<void*>(fd));
-            }
-
             long SslGateway::BIO_get_ssl_(BIO* bp, SSL** ssl)
             {
                 return BIO_ctrl_(bp, BIO_C_GET_SSL, 0, reinterpret_cast<void*>(ssl));
@@ -693,18 +878,6 @@
 
                 fp(e, buf, len);
             }
-
-            void SslGateway::ERR_print_errors_fp_(FILE* fd)
-            {
-                if (!functions.fpERR_print_errors_fp)
-                    return;
-
-                typedef void (FuncType)(FILE*);
-
-                FuncType* fp = reinterpret_cast<FuncType*>(functions.fpERR_print_errors_fp);
-
-                fp(fd);
-            }
         }
     }
 }
diff --git a/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.h b/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.h
index 30134f3..d6457f6 100644
--- a/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.h
+++ b/modules/platforms/cpp/network/src/network/ssl/ssl_gateway.h
@@ -18,6 +18,7 @@
 #ifndef _IGNITE_NETWORK_SSL_SSL_GATEWAY
 #define _IGNITE_NETWORK_SSL_SSL_GATEWAY
 
+#include <string>
 #include <openssl/ssl.h>
 #include <openssl/conf.h>
 #include <openssl/err.h>
@@ -42,6 +43,8 @@
                 void *fpSSL_CTX_free;
                 void *fpSSL_CTX_set_verify;
                 void *fpSSL_CTX_set_verify_depth;
+                void *fpSSL_CTX_set_cert_store;
+                void *fpSSL_CTX_set_default_verify_paths;
                 void *fpSSL_CTX_load_verify_locations;
                 void *fpSSL_CTX_use_certificate_chain_file;
                 void *fpSSL_CTX_use_RSAPrivateKey_file;
@@ -54,22 +57,31 @@
                 void *fpSSL_ctrl;
                 void *fpSSLv23_client_method;
                 void *fpSSL_set_connect_state;
+                void *fpSSL_set_bio;
                 void *fpSSL_connect;
                 void *fpSSL_get_error;
                 void *fpSSL_want;
                 void *fpSSL_write;
                 void *fpSSL_read;
                 void *fpSSL_pending;
+                void *fpSSL_get_version;
                 void *fpSSL_get_fd;
+                void *fpSSL_new;
                 void *fpSSL_free;
                 void *fpOPENSSL_config;
+                void *fpX509_STORE_new;
+                void *fpX509_STORE_add_cert;
+                void *fpd2i_X509;
                 void *fpX509_free;
+                void *fpBIO_new;
                 void *fpBIO_new_ssl_connect;
                 void *fpBIO_free_all;
+                void *fpBIO_s_mem;
+                void *fpBIO_read;
+                void *fpBIO_write;
                 void *fpBIO_ctrl;
                 void *fpERR_get_error;
                 void *fpERR_error_string_n;
-                void *fpERR_print_errors_fp;
 
                 void *fpOpenSSL_version;
                 void *fpSSL_CTX_set_options;
@@ -97,15 +109,6 @@
                 void LoadAll();
 
                 /**
-                 * Get functions.
-                 * @return Functions structure.
-                 */
-                SslFunctions& GetFunctions()
-                {
-                    return functions;
-                }
-
-                /**
                  * Check whether the libraries are loaded.
                  * @return @c true if loaded.
                  */
@@ -114,7 +117,7 @@
                     return inited;
                 }
 
-                char* SSLeay_version_(int type);
+                char* OpenSSL_version_(int type);
 
                 int OPENSSL_init_ssl_(uint64_t opts, const void* settings);
 
@@ -130,6 +133,10 @@
 
                 void SSL_CTX_set_verify_depth_(SSL_CTX* ctx, int depth);
 
+                void SSL_CTX_set_cert_store_(SSL_CTX* ctx, X509_STORE* store);
+
+                int SSL_CTX_set_default_verify_paths_(SSL_CTX* ctx);
+
                 int SSL_CTX_load_verify_locations_(SSL_CTX* ctx, const char* cAfile, const char* cApath);
 
                 int SSL_CTX_use_certificate_chain_file_(SSL_CTX* ctx, const char* file);
@@ -154,6 +161,8 @@
 
                 int SSL_connect_(SSL* s);
 
+                void SSL_set_bio_(SSL* s, BIO* rbio, BIO* wbio);
+
                 int SSL_get_error_(const SSL* s, int ret);
 
                 int SSL_want_(const SSL* s);
@@ -164,8 +173,12 @@
 
                 int SSL_pending_(const SSL* ssl);
 
+                const char* SSL_get_version_(const SSL* ssl);
+
                 int SSL_get_fd_(const SSL* ssl);
 
+                SSL* SSL_new_(SSL_CTX* ctx);
+
                 void SSL_free_(SSL* ssl);
 
                 const SSL_METHOD* SSLv23_client_method_();
@@ -174,15 +187,29 @@
 
                 void OPENSSL_config_(const char* configName);
 
-                void X509_free_(X509* a);
+                X509_STORE* X509_STORE_new_();
+
+                int X509_STORE_add_cert_(X509_STORE* ctx, X509* cert);
+
+                X509* d2i_X509_(X509** cert, const unsigned char** ppin, long length);
+
+                void X509_free_(X509* cert);
+
+                BIO* BIO_new_(const BIO_METHOD* method);
 
                 BIO* BIO_new_ssl_connect_(SSL_CTX* ctx);
 
                 void BIO_free_all_(BIO* a);
 
-                long BIO_ctrl_(BIO* bp, int cmd, long larg, void* parg);
+                const BIO_METHOD* BIO_s_mem_();
 
-                long BIO_get_fd_(BIO* bp, int* fd);
+                int BIO_read_(BIO* b, void* data, int len);
+
+                int BIO_write_(BIO* b, const void *data, int len);
+
+                int BIO_pending_(BIO* b);
+
+                long BIO_ctrl_(BIO* bp, int cmd, long larg, void* parg);
 
                 long BIO_get_ssl_(BIO* bp, SSL** ssl);
 
@@ -194,8 +221,6 @@
 
                 void ERR_error_string_n_(unsigned long e, char* buf, size_t len);
 
-                void ERR_print_errors_fp_(FILE *fd);
-
             private:
                 /**
                  * Constructor.
@@ -215,9 +240,10 @@
                 /**
                  * Load SSL library.
                  * @param name Name.
+                 * @param homeDir OpenSSL home directory.
                  * @return Module.
                  */
-                common::dynamic::Module LoadSslLibrary(const char* name);
+                static common::dynamic::Module LoadSslLibrary(const std::string& name, const std::string& homeDir);
 
                 /**
                  * Load all SSL libraries.
@@ -225,6 +251,13 @@
                 void LoadSslLibraries();
 
                 /**
+                 * Try load SSL libraries
+                 * @param homeDir OpenSSL home directory.
+                 * @return @c true on success and @c false if not.
+                 */
+                bool TryLoadSslLibraries(const std::string& homeDir);
+
+                /**
                  * Load mandatory SSL methods.
                  *
                  * @throw IgniteError if can not load one of the functions.
@@ -274,4 +307,4 @@
     }
 }
 
-#endif //_IGNITE_NETWORK_SSL_SSL_GATEWAY
\ No newline at end of file
+#endif //_IGNITE_NETWORK_SSL_SSL_GATEWAY
diff --git a/modules/platforms/cpp/network/src/network/tcp_socket_client.h b/modules/platforms/cpp/network/src/network/tcp_socket_client.h
index 8627d99..c034b76 100644
--- a/modules/platforms/cpp/network/src/network/tcp_socket_client.h
+++ b/modules/platforms/cpp/network/src/network/tcp_socket_client.h
@@ -39,12 +39,6 @@
             /** Buffers size */
             enum { BUFFER_SIZE = 0x10000 };
 
-            /** The time in seconds the connection needs to remain idle before starts sending keepalive probes. */
-            enum { KEEP_ALIVE_IDLE_TIME = 60 };
-
-            /** The time in seconds between individual keepalive probes. */
-            enum { KEEP_ALIVE_PROBES_PERIOD = 1 };
-
             /**
              * Constructor.
              */
@@ -105,11 +99,6 @@
             void InternalClose();
 
             /**
-             * Tries set socket options.
-             */
-            void TrySetOptions();
-
-            /**
              * Wait on the socket for any event for specified time.
              * This function uses poll to achive timeout functionality
              * for every separate socket operation.
diff --git a/modules/platforms/cpp/odbc-test/CMakeLists.txt b/modules/platforms/cpp/odbc-test/CMakeLists.txt
index 51d1501..b3a8af9 100644
--- a/modules/platforms/cpp/odbc-test/CMakeLists.txt
+++ b/modules/platforms/cpp/odbc-test/CMakeLists.txt
@@ -28,9 +28,10 @@
 find_package(ODBC REQUIRED)
 
 include_directories(SYSTEM ${ODBC_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${JNI_INCLUDE_DIRS})
-include_directories(include ../odbc/include ../network/include)
+include_directories(include ../odbc/include ../network/include ../network/src)
 
-set(SOURCES src/teamcity/teamcity_boost.cpp
+set(SOURCES
+        src/teamcity/teamcity_boost.cpp
         src/teamcity/teamcity_messages.cpp
         src/parser_test.cpp
         src/cursor_test.cpp
@@ -67,6 +68,7 @@
         src/sql_parsing_test.cpp
         src/streaming_test.cpp
         src/cursor_binding_test.cpp
+        src/sql_schema_test.cpp
         src/test_server.cpp
         ../odbc/src/log.cpp
         ../odbc/src/cursor.cpp
@@ -104,4 +106,9 @@
 
 add_test(NAME ${TEST_TARGET} COMMAND ${TARGET} --catch_system_errors=no --log_level=all)
 
-set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT IGNITE_NATIVE_TEST_ODBC_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config)
+list(APPEND ENV_VARIABLES "IGNITE_NATIVE_TEST_ODBC_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config")
+if (${WITH_SANITIZERS})
+    list(APPEND ENV_VARIABLES ${SANITIZERS_ENV})
+endif()
+
+set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT "${ENV_VARIABLES}")
diff --git a/modules/platforms/cpp/odbc-test/config/queries-schema-32.xml b/modules/platforms/cpp/odbc-test/config/queries-schema-32.xml
new file mode 100644
index 0000000..c564301
--- /dev/null
+++ b/modules/platforms/cpp/odbc-test/config/queries-schema-32.xml
@@ -0,0 +1,52 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="queries-schema-default.xml"/>
+
+    <bean parent="grid.cfg">
+        <property name="memoryConfiguration">
+            <bean class="org.apache.ignite.configuration.MemoryConfiguration">
+                <property name="systemCacheInitialSize" value="#{10 * 1024 * 1024}"/>
+                <property name="systemCacheMaxSize" value="#{40 * 1024 * 1024}"/>
+                <property name="defaultMemoryPolicyName" value="dfltPlc"/>
+
+                <property name="memoryPolicies">
+                    <list>
+                        <bean class="org.apache.ignite.configuration.MemoryPolicyConfiguration">
+                            <property name="name" value="dfltPlc"/>
+                            <property name="maxSize" value="#{100 * 1024 * 1024}"/>
+                            <property name="initialSize" value="#{10 * 1024 * 1024}"/>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/odbc-test/config/queries-schema-default.xml b/modules/platforms/cpp/odbc-test/config/queries-schema-default.xml
new file mode 100644
index 0000000..bcdaec5
--- /dev/null
+++ b/modules/platforms/cpp/odbc-test/config/queries-schema-default.xml
@@ -0,0 +1,68 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <bean abstract="true" id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <property name="localHost" value="127.0.0.1"/>
+        <property name="connectorConfiguration"><null/></property>
+
+        <property name="clientConnectorConfiguration">
+            <bean class="org.apache.ignite.configuration.ClientConnectorConfiguration">
+                <property name="host" value="127.0.0.1"/>
+                <property name="port" value="11110"/>
+                <property name="portRange" value="10"/>
+            </bean>
+        </property>
+
+        <property name="sqlSchemas">
+            <list>
+                <value>SCHEMA_1</value>
+                <value>SCHEMA_2</value>
+                <value>"ScHeMa3"</value>
+                <value>SCHEMA_4</value>
+            </list>
+        </property>
+
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+                <property name="socketTimeout" value="300" />
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/odbc-test/config/queries-schema.xml b/modules/platforms/cpp/odbc-test/config/queries-schema.xml
new file mode 100644
index 0000000..1a9513f
--- /dev/null
+++ b/modules/platforms/cpp/odbc-test/config/queries-schema.xml
@@ -0,0 +1,34 @@
+<?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 cache.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="queries-schema-default.xml"/>
+
+    <bean parent="grid.cfg"/>
+</beans>
diff --git a/modules/platforms/cpp/odbc-test/src/sql_schema_test.cpp b/modules/platforms/cpp/odbc-test/src/sql_schema_test.cpp
new file mode 100644
index 0000000..adf7e2c
--- /dev/null
+++ b/modules/platforms/cpp/odbc-test/src/sql_schema_test.cpp
@@ -0,0 +1,337 @@
+/*
+ * 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.
+ */
+
+#include <stdint.h>
+
+#include <iterator>
+
+#include <boost/test/unit_test.hpp>
+
+#include <ignite/common/utils.h>
+
+#include "ignite/cache/cache.h"
+#include "ignite/cache/query/query_cursor.h"
+#include "ignite/cache/query/query_sql_fields.h"
+#include "ignite/ignite.h"
+#include "ignite/ignition.h"
+
+#include "odbc_test_suite.h"
+#include "test_utils.h"
+#include "ignite/odbc/odbc_error.h"
+
+using namespace boost::unit_test;
+
+using namespace ignite;
+using namespace ignite::cache;
+using namespace ignite::cache::query;
+
+using namespace ignite_test;
+
+/**
+ * Ensure that cursor is empy fails.
+ *
+ * @param stmt Statement.
+ */
+void ChechEmptyCursorGetNextThrowsException(SQLHSTMT stmt)
+{
+    SQLRETURN ret = SQLFetch(stmt);
+
+    BOOST_REQUIRE_EQUAL(ret, SQL_NO_DATA);
+}
+
+/**
+ * Check single row through iteration.
+ *
+ * @param stmt Statement.
+ * @param c1 First column.
+ */
+void CheckSingleLongRow1(SQLHSTMT stmt, const int64_t c1)
+{
+    int64_t val1 = 0;
+
+    SQLRETURN ret = SQLFetch(stmt);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    ret = SQLGetData(stmt, 1, SQL_C_SBIGINT, &val1, 0, 0);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    BOOST_REQUIRE_EQUAL(val1, c1);
+
+    ChechEmptyCursorGetNextThrowsException(stmt);
+}
+
+/**
+ * Check row through iteration.
+ *
+ * @param stmt Statement.
+ * @param c1 First column.
+ * @param c2 Second column.
+ */
+void CheckStringRow2(SQLHSTMT stmt, const std::string& c1, const std::string& c2)
+{
+    char val1[1024] = { 0 };
+    char val2[1024] = { 0 };
+    SQLLEN val1Len = 0;
+    SQLLEN val2Len = 0;
+
+    SQLRETURN ret = SQLFetch(stmt);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    ret = SQLGetData(stmt, 1, SQL_C_CHAR, val1, sizeof(val1), &val1Len);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    ret = SQLGetData(stmt, 2, SQL_C_CHAR, val2, sizeof(val2), &val2Len);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    BOOST_REQUIRE_EQUAL(std::string(val1, static_cast<size_t>(val1Len)), c1);
+    BOOST_REQUIRE_EQUAL(std::string(val2, static_cast<size_t>(val2Len)), c2);
+}
+
+/**
+ * Check single row through iteration.
+ *
+ * @param stmt Statement.
+ * @param c1 First column.
+ * @param c2 Second column.
+ */
+void CheckSingleIntRow2(SQLHSTMT stmt, int32_t c1, int32_t c2)
+{
+    int32_t val1 = 0;
+    int32_t val2 = 0;
+
+    SQLRETURN ret = SQLFetch(stmt);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    ret = SQLGetData(stmt, 1, SQL_C_SLONG, &val1, 0, 0);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    ret = SQLGetData(stmt, 2, SQL_C_SLONG, &val2, 0, 0);
+    if (!SQL_SUCCEEDED(ret))
+        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+    BOOST_REQUIRE_EQUAL(val1, c1);
+    BOOST_REQUIRE_EQUAL(val2, c2);
+
+    ChechEmptyCursorGetNextThrowsException(stmt);
+}
+
+static const std::string TABLE_NAME = "T1";
+static const std::string SCHEMA_NAME_1 = "SCHEMA_1";
+static const std::string SCHEMA_NAME_2 = "SCHEMA_2";
+static const std::string SCHEMA_NAME_3 = "ScHeMa3";
+static const std::string Q_SCHEMA_NAME_3 = '"' + SCHEMA_NAME_3 + '"';
+static const std::string SCHEMA_NAME_4 = "SCHEMA_4";
+
+/**
+ * Test setup fixture.
+ */
+struct SchemaTestSuiteFixture : odbc::OdbcTestSuite
+{
+    /**
+     * Constructor.
+     */
+    SchemaTestSuiteFixture() :
+        grid(StartNode("queries-schema.xml", "Node1"))
+    {
+        Connect("DRIVER={Apache Ignite};address=127.0.0.1:11110;schema=PUBLIC");
+    }
+
+    /**
+     * Destructor.
+     */
+    ~SchemaTestSuiteFixture()
+    {
+        Ignition::StopAll(true);
+    }
+
+    /** Perform SQL in cluster. */
+    void Sql(const std::string& sql)
+    {
+        SQLFreeStmt(stmt, SQL_CLOSE);
+
+        SQLRETURN ret = ExecQuery(sql);
+
+        ODBC_THROW_ON_ERROR(ret, SQL_HANDLE_STMT, stmt);
+    }
+
+    std::string TableName(bool withSchema)
+    {
+        return withSchema ? "PUBLIC." + TABLE_NAME : TABLE_NAME;
+    }
+
+    template<typename Predicate>
+    void ExecuteStatementsAndVerify(Predicate& pred)
+    {
+        Sql("CREATE TABLE " + TableName(pred()) + " (id INT PRIMARY KEY, val INT)");
+
+        Sql("CREATE INDEX t1_idx_1 ON " + TableName(pred()) + "(val)");
+
+        Sql("INSERT INTO " + TableName(pred()) + " (id, val) VALUES(1, 2)");
+
+        Sql("SELECT * FROM " + TableName(pred()));
+        CheckSingleIntRow2(stmt, 1, 2);
+
+        Sql("UPDATE " + TableName(pred()) + " SET val = 5");
+        Sql("SELECT * FROM " + TableName(pred()));
+        CheckSingleIntRow2(stmt, 1, 5);
+
+        Sql("DELETE FROM " + TableName(pred()) + " WHERE id = 1");
+        Sql("SELECT COUNT(*) FROM " + TableName(pred()));
+        CheckSingleLongRow1(stmt, 0);
+
+        Sql("SELECT COUNT(*) FROM SYS.TABLES WHERE schema_name = 'PUBLIC' "
+            "AND table_name = \'" + TABLE_NAME + "\'");
+        CheckSingleLongRow1(stmt, 1);
+
+        Sql("DROP TABLE " + TableName(pred()));
+    }
+
+    /** Node started during the test. */
+    Ignite grid;
+};
+
+BOOST_FIXTURE_TEST_SUITE(SchemaTestSuite, SchemaTestSuiteFixture)
+
+bool TruePred()
+{
+    return true;
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsExplicitPublicSchema)
+{
+    ExecuteStatementsAndVerify(TruePred);
+}
+
+bool FalsePred()
+{
+    return false;
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsImplicitPublicSchema)
+{
+    ExecuteStatementsAndVerify(FalsePred);
+}
+
+struct MixedPred
+{
+    int i;
+
+    MixedPred() : i(0)
+    {
+        // No-op.
+    }
+
+    bool operator()()
+    {
+        return (++i & 1) == 0;
+    }
+};
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsMixedPublicSchema)
+{
+    MixedPred pred;
+
+    ExecuteStatementsAndVerify(pred);
+}
+
+BOOST_AUTO_TEST_CASE(TestCreateDropNonExistingSchema)
+{
+    BOOST_CHECK_THROW(
+        Sql("CREATE TABLE UNKNOWN_SCHEMA." + TABLE_NAME + "(id INT PRIMARY KEY, val INT)"),
+        OdbcClientError
+    );
+
+    BOOST_CHECK_THROW(
+        Sql("DROP TABLE UNKNOWN_SCHEMA." + TABLE_NAME),
+        OdbcClientError
+    );
+}
+
+BOOST_AUTO_TEST_CASE(TestBasicOpsDiffSchemas)
+{
+    Sql("CREATE TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " (s1_key INT PRIMARY KEY, s1_val INT)");
+    Sql("CREATE TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " (s2_key INT PRIMARY KEY, s2_val INT)");
+    Sql("CREATE TABLE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " (s3_key INT PRIMARY KEY, s3_val INT)");
+    Sql("CREATE TABLE " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " (s4_key INT PRIMARY KEY, s4_val INT)");
+
+    Sql("INSERT INTO " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " (s1_key, s1_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " (s2_key, s2_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " (s3_key, s3_val) VALUES (1, 2)");
+    Sql("INSERT INTO " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " (s4_key, s4_val) VALUES (1, 2)");
+
+    Sql("UPDATE " + SCHEMA_NAME_1 + '.' + TABLE_NAME + " SET s1_val = 5");
+    Sql("UPDATE " + SCHEMA_NAME_2 + '.' + TABLE_NAME + " SET s2_val = 5");
+    Sql("UPDATE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + " SET s3_val = 5");
+    Sql("UPDATE " + SCHEMA_NAME_4 + '.' + TABLE_NAME + " SET s4_val = 5");
+
+    Sql("DELETE FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("DELETE FROM " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_1 + '.' + TABLE_NAME + "(s1_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_2 + '.' + TABLE_NAME + "(s2_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME + "(s3_val)");
+    Sql("CREATE INDEX t1_idx_1 ON " + SCHEMA_NAME_4 + '.' + TABLE_NAME + "(s4_val)");
+
+    Sql("SELECT * FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("SELECT * FROM " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    Sql("SELECT * FROM " + SCHEMA_NAME_1 + '.' + TABLE_NAME
+      + " JOIN " + SCHEMA_NAME_2 + '.' + TABLE_NAME
+      + " JOIN " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME
+      + " JOIN " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+
+    Sql("SELECT SCHEMA_NAME, KEY_ALIAS FROM SYS.TABLES ORDER BY SCHEMA_NAME");
+
+    CheckStringRow2(stmt, SCHEMA_NAME_1, "S1_KEY");
+    CheckStringRow2(stmt, SCHEMA_NAME_2, "S2_KEY");
+    CheckStringRow2(stmt, SCHEMA_NAME_4, "S4_KEY");
+    CheckStringRow2(stmt, SCHEMA_NAME_3, "S3_KEY");
+
+    ChechEmptyCursorGetNextThrowsException(stmt);
+
+    Sql("DROP TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + Q_SCHEMA_NAME_3 + '.' + TABLE_NAME);
+    Sql("DROP TABLE " + SCHEMA_NAME_4 + '.' + TABLE_NAME);
+}
+
+BOOST_AUTO_TEST_CASE(TestCreateTblsInDiffSchemasForSameCache)
+{
+    std::string testCache = "cache1";
+
+    Sql("CREATE TABLE " + SCHEMA_NAME_1 + '.' + TABLE_NAME +
+        " (s1_key INT PRIMARY KEY, s1_val INT) WITH \"cache_name=" + testCache + '"');
+
+    BOOST_CHECK_THROW(
+        Sql("CREATE TABLE " + SCHEMA_NAME_2 + '.' + TABLE_NAME +
+            " (s1_key INT PRIMARY KEY, s2_val INT) WITH \"cache_name=" + testCache + '"'),
+        OdbcClientError
+    );
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/odbc-test/src/sql_types_test.cpp b/modules/platforms/cpp/odbc-test/src/sql_types_test.cpp
index 35cf33c..565de11 100644
--- a/modules/platforms/cpp/odbc-test/src/sql_types_test.cpp
+++ b/modules/platforms/cpp/odbc-test/src/sql_types_test.cpp
@@ -153,7 +153,7 @@
     BOOST_REQUIRE_EQUAL_COLLECTIONS(out.i8ArrayField.begin(), out.i8ArrayField.end(), paramData.begin(), paramData.end());
 }
 
-BOOST_AUTO_TEST_CASE(TestStingParamNullLen)
+BOOST_AUTO_TEST_CASE(TestStringParamNullLen)
 {
     SQLRETURN ret;
 
@@ -180,7 +180,7 @@
     SQLLEN paramLen = static_cast<SQLLEN>(paramData.size());
 
     ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR,
-       paramData.size(), 0, &paramData[0], paramLen, 0);
+       paramData.size(), 0, &paramData[0], paramLen, &paramLen);
 
     if (!SQL_SUCCEEDED(ret))
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
diff --git a/modules/platforms/cpp/odbc-test/src/streaming_test.cpp b/modules/platforms/cpp/odbc-test/src/streaming_test.cpp
index a7e6c1c..a2c8a93 100644
--- a/modules/platforms/cpp/odbc-test/src/streaming_test.cpp
+++ b/modules/platforms/cpp/odbc-test/src/streaming_test.cpp
@@ -262,6 +262,22 @@
             BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
     }
 
+    // Performs additional checks for set streaming parameter query.
+    void ExecSetStreamingQuery(const std::string& query) {
+        SQLRETURN ret = ExecQuery(query);
+
+        if (!SQL_SUCCEEDED(ret))
+            BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+        SQLSMALLINT columnCount;
+        ret = SQLNumResultCols(stmt, &columnCount);
+
+        if (!SQL_SUCCEEDED(ret))
+            BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+
+        BOOST_ASSERT(columnCount == 0);
+    }
+
     /** Node started during the test. */
     Ignite grid;
 
@@ -275,10 +291,7 @@
 {
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery("set streaming on batch_size 100 flush_frequency 100");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming on batch_size 100 flush_frequency 100");
 
     InsertTestStrings(0, 10);
 
@@ -286,10 +299,7 @@
 
     InsertTestStrings(10, 110);
 
-    res = ExecQuery("set streaming off");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming off");
 
     BOOST_CHECK_EQUAL(cache.Size(), 110);
 
@@ -300,7 +310,7 @@
 {
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery(
+    ExecSetStreamingQuery(
         "set streaming 1 "
         "allow_overwrite on "
         "batch_size 512 "
@@ -309,19 +319,13 @@
         "flush_frequency 100 "
         "ordered");
 
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
-
     InsertTestStrings(0, 10);
 
     BOOST_CHECK_EQUAL(cache.Size(), 0);
 
     InsertTestStrings(0, 512);
 
-    res = ExecQuery("set streaming off");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming off");
 
     BOOST_CHECK_EQUAL(cache.Size(), 512);
 }
@@ -330,10 +334,7 @@
 {
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery("set streaming 1 allow_overwrite off batch_size 10");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 allow_overwrite off batch_size 10");
 
     InsertTestStrings(0, 10);
 
@@ -341,10 +342,7 @@
 
     InsertTestStrings(0, 10);
 
-    res = ExecQuery("set streaming off");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming off");
 
     BOOST_CHECK_EQUAL(cache.Size(), 10);
 }
@@ -353,10 +351,7 @@
 {
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery("set streaming 1 batch_size 100 flush_frequency 1000");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 100 flush_frequency 1000");
 
     InsertTestStrings(0, 10);
 
@@ -366,10 +361,7 @@
 
     BOOST_CHECK_EQUAL(cache.Size(), 0);
 
-    res = ExecQuery("set streaming 1 batch_size 10 flush_frequency 100");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 10 flush_frequency 100");
 
     BOOST_CHECK_EQUAL(cache.Size(), 20);
 
@@ -377,10 +369,7 @@
 
     BOOST_CHECK_EQUAL(cache.Size(), 20);
 
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), 50);
 }
@@ -389,10 +378,7 @@
 {
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery("set streaming 1 batch_size 100 flush_frequency 1000");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 100 flush_frequency 1000");
 
     InsertTestStrings(0, 10);
 
@@ -402,15 +388,12 @@
 
     BOOST_CHECK_EQUAL(cache.Size(), 0);
 
-    res = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
+    SQLRETURN res = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
 
     if (res != SQL_SUCCESS)
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
 
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), 10);
 }
@@ -426,10 +409,7 @@
     if (res != SQL_SUCCESS)
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
 
-    res = ExecQuery("set streaming 1 batch_size 100 flush_frequency 1000");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 100 flush_frequency 1000");
 
     InsertTestStrings(0, 10);
 
@@ -441,10 +421,7 @@
 
     BOOST_CHECK_EQUAL(cache.Size(), 0);
 
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), 50);
 
@@ -465,10 +442,7 @@
     if (res != SQL_SUCCESS)
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
 
-    res = ExecQuery("set streaming 1 batch_size 100 flush_frequency 1000");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 100 flush_frequency 1000");
 
     InsertTestStrings(0, 10);
 
@@ -497,10 +471,7 @@
     if (res != SQL_SUCCESS)
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
 
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), 50);
 }
@@ -516,10 +487,7 @@
     if (res != SQL_SUCCESS)
         BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
 
-    res = ExecQuery("set streaming 1 batch_size 100 flush_frequency 1000");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 1 batch_size 100 flush_frequency 1000");
 
     InsertTestStrings2(0, 10);
 
@@ -538,10 +506,7 @@
 
     BOOST_CHECK_EQUAL(cache.Size(), 0);
 
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), 50);
 
@@ -558,20 +523,11 @@
 
     Connect("DRIVER={Apache Ignite};SERVER=127.0.0.1;PORT=11110;SCHEMA=cache");
 
-    SQLRETURN res = ExecQuery("set streaming on");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming on");
 
     InsertTestStrings(0, OBJECT_NUM);
 
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
-
-    res = ExecQuery("set streaming 0");
-
-    if (res != SQL_SUCCESS)
-        BOOST_FAIL(GetOdbcErrorMessage(SQL_HANDLE_STMT, stmt));
+    ExecSetStreamingQuery("set streaming 0");
 
     BOOST_CHECK_EQUAL(cache.Size(), OBJECT_NUM);
 
diff --git a/modules/platforms/cpp/odbc/include/ignite/odbc/connection.h b/modules/platforms/cpp/odbc/include/ignite/odbc/connection.h
index b670ee0..1e13348 100644
--- a/modules/platforms/cpp/odbc/include/ignite/odbc/connection.h
+++ b/modules/platforms/cpp/odbc/include/ignite/odbc/connection.h
@@ -99,7 +99,7 @@
              *
              * @param cfg Configuration.
              */
-            void Establish(const config::Configuration cfg);
+            void Establish(const config::Configuration& cfg);
 
             /**
              * Release established connection.
diff --git a/modules/platforms/cpp/odbc/include/ignite/odbc/query/internal_query.h b/modules/platforms/cpp/odbc/include/ignite/odbc/query/internal_query.h
index d697999..0b37a50 100644
--- a/modules/platforms/cpp/odbc/include/ignite/odbc/query/internal_query.h
+++ b/modules/platforms/cpp/odbc/include/ignite/odbc/query/internal_query.h
@@ -114,16 +114,6 @@
                 }
 
                 /**
-                 * Get column metadata.
-                 *
-                 * @return Column metadata.
-                 */
-                virtual const meta::ColumnMetaVector* GetMeta()
-                {
-                    return 0;
-                }
-
-                /**
                  * Check if data is available.
                  *
                  * @return True if data is available.
diff --git a/modules/platforms/cpp/odbc/include/ignite/odbc/query/query.h b/modules/platforms/cpp/odbc/include/ignite/odbc/query/query.h
index 4e64a21..c70f820 100644
--- a/modules/platforms/cpp/odbc/include/ignite/odbc/query/query.h
+++ b/modules/platforms/cpp/odbc/include/ignite/odbc/query/query.h
@@ -120,7 +120,12 @@
                  *
                  * @return Column metadata.
                  */
-                virtual const meta::ColumnMetaVector* GetMeta() = 0;
+                virtual const meta::ColumnMetaVector* GetMeta()
+                {
+                    static const meta::ColumnMetaVector empty;
+
+                    return &empty;
+                }
 
                 /**
                  * Check if data is available.
diff --git a/modules/platforms/cpp/odbc/include/ignite/odbc/query/streaming_query.h b/modules/platforms/cpp/odbc/include/ignite/odbc/query/streaming_query.h
index 285d3fb..a18a819 100644
--- a/modules/platforms/cpp/odbc/include/ignite/odbc/query/streaming_query.h
+++ b/modules/platforms/cpp/odbc/include/ignite/odbc/query/streaming_query.h
@@ -61,13 +61,6 @@
                 virtual SqlResult::Type Execute();
 
                 /**
-                 * Get column metadata.
-                 *
-                 * @return Column metadata.
-                 */
-                virtual const meta::ColumnMetaVector* GetMeta();
-
-                /**
                  * Fetch next result row to application buffers.
                  *
                  * @param columnBindings Application buffers to put data to.
diff --git a/modules/platforms/cpp/odbc/include/ignite/odbc/utility.h b/modules/platforms/cpp/odbc/include/ignite/odbc/utility.h
index 152da66..44977bc 100644
--- a/modules/platforms/cpp/odbc/include/ignite/odbc/utility.h
+++ b/modules/platforms/cpp/odbc/include/ignite/odbc/utility.h
@@ -101,14 +101,6 @@
          * @return Standard string containing the same data.
          */
         std::string SqlStringToString(const unsigned char* sqlStr, int32_t sqlStrLen);
-
-        /**
-         * Convert binary data to hex dump form
-         * @param data  pointer to data
-         * @param count data length
-         * @return standard string containing the formated hex dump
-         */
-        std::string HexDump(const void* data, size_t count);
     }
 }
 
diff --git a/modules/platforms/cpp/odbc/src/app/application_data_buffer.cpp b/modules/platforms/cpp/odbc/src/app/application_data_buffer.cpp
index 2942def..da48bbe 100644
--- a/modules/platforms/cpp/odbc/src/app/application_data_buffer.cpp
+++ b/modules/platforms/cpp/odbc/src/app/application_data_buffer.cpp
@@ -1195,6 +1195,14 @@
                 return ApplyOffset(reslen, sizeof(*reslen));
             }
 
+
+            template<typename T, typename V>
+            inline V LoadPrimitive(const void* data) {
+                T res = T();
+                std::memcpy(&res, data, sizeof(res));
+                return static_cast<V>(res);
+            }
+
             template<typename T>
             T ApplicationDataBuffer::GetNum() const
             {
@@ -1235,62 +1243,62 @@
 
                     case OdbcNativeType::AI_SIGNED_TINYINT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const signed char*>(GetData()));
+                        res = LoadPrimitive<int8_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_BIT:
                     case OdbcNativeType::AI_UNSIGNED_TINYINT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const unsigned char*>(GetData()));
+                        res = LoadPrimitive<uint8_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_SIGNED_SHORT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const signed short*>(GetData()));
+                        res = LoadPrimitive<int16_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_UNSIGNED_SHORT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const unsigned short*>(GetData()));
+                        res = LoadPrimitive<uint16_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_SIGNED_LONG:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const signed long*>(GetData()));
+                        res = LoadPrimitive<int32_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_UNSIGNED_LONG:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const unsigned long*>(GetData()));
+                        res = LoadPrimitive<uint32_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_SIGNED_BIGINT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const int64_t*>(GetData()));
+                        res = LoadPrimitive<int64_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_UNSIGNED_BIGINT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const uint64_t*>(GetData()));
+                        res = LoadPrimitive<uint64_t, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_FLOAT:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const float*>(GetData()));
+                        res = LoadPrimitive<float, T>(GetData());
                         break;
                     }
 
                     case OdbcNativeType::AI_DOUBLE:
                     {
-                        res = static_cast<T>(*reinterpret_cast<const double*>(GetData()));
+                        res = LoadPrimitive<double, T>(GetData());
                         break;
                     }
 
diff --git a/modules/platforms/cpp/odbc/src/connection.cpp b/modules/platforms/cpp/odbc/src/connection.cpp
index ba94e0b..2e6a7df 100644
--- a/modules/platforms/cpp/odbc/src/connection.cpp
+++ b/modules/platforms/cpp/odbc/src/connection.cpp
@@ -136,7 +136,7 @@
             return InternalEstablish(config);
         }
 
-        void Connection::Establish(const config::Configuration cfg)
+        void Connection::Establish(const config::Configuration& cfg)
         {
             IGNITE_ODBC_API_CALL(InternalEstablish(cfg));
         }
@@ -147,7 +147,7 @@
 
             if (sslMode == ssl::SslMode::DISABLE)
             {
-                socket.reset(network::ssl::MakeTcpSocketClient());
+                socket.reset(network::MakeTcpSocketClient());
 
                 return SqlResult::AI_SUCCESS;
             }
@@ -165,8 +165,12 @@
                 return SqlResult::AI_ERROR;
             }
 
-            socket.reset(network::ssl::MakeSecureSocketClient(
-                config.GetSslCertFile(), config.GetSslKeyFile(), config.GetSslCaFile()));
+            network::ssl::SecureConfiguration sslCfg;
+            sslCfg.certPath = config.GetSslCertFile();
+            sslCfg.keyPath = config.GetSslKeyFile();
+            sslCfg.caPath = config.GetSslCaFile();
+
+            socket.reset(network::ssl::MakeSecureSocketClient(sslCfg));
 
             return SqlResult::AI_SUCCESS;
         }
@@ -288,7 +292,7 @@
                 throw OdbcError(SqlState::S08S01_LINK_FAILURE, "Can not send message due to connection failure");
 
 #ifdef PER_BYTE_DEBUG
-            LOG_MSG("message sent: (" <<  msg.GetSize() << " bytes)" << utility::HexDump(msg.GetData(), msg.GetSize()));
+            LOG_MSG("message sent: (" <<  msg.GetSize() << " bytes)" << common::HexDump(msg.GetData(), msg.GetSize()));
 #endif //PER_BYTE_DEBUG
 
             return true;
@@ -357,7 +361,7 @@
                 throw OdbcError(SqlState::S08S01_LINK_FAILURE, "Can not receive message body");
 
 #ifdef PER_BYTE_DEBUG
-            LOG_MSG("Message received: " << utility::HexDump(&msg[0], msg.size()));
+            LOG_MSG("Message received: " << common::HexDump(&msg[0], msg.size()));
 #endif //PER_BYTE_DEBUG
 
             return true;
diff --git a/modules/platforms/cpp/odbc/src/query/streaming_query.cpp b/modules/platforms/cpp/odbc/src/query/streaming_query.cpp
index 4bc19b1..282ca64 100644
--- a/modules/platforms/cpp/odbc/src/query/streaming_query.cpp
+++ b/modules/platforms/cpp/odbc/src/query/streaming_query.cpp
@@ -49,11 +49,6 @@
                 return connection.GetStreamingContext().Execute(sql, params);
             }
 
-            const meta::ColumnMetaVector* StreamingQuery::GetMeta()
-            {
-                return 0;
-            }
-
             SqlResult::Type StreamingQuery::FetchNextRow(app::ColumnBindingMap&)
             {
                 return SqlResult::AI_NO_DATA;
diff --git a/modules/platforms/cpp/odbc/src/utility.cpp b/modules/platforms/cpp/odbc/src/utility.cpp
index c060a0a..6f34d10 100644
--- a/modules/platforms/cpp/odbc/src/utility.cpp
+++ b/modules/platforms/cpp/odbc/src/utility.cpp
@@ -157,21 +157,6 @@
             else
                 res.clear();
         }
-
-        std::string HexDump(const void* data, size_t count)
-        {
-            std::stringstream  dump;
-            size_t cnt = 0;
-            for(const uint8_t* p = (const uint8_t*)data, *e = (const uint8_t*)data + count; p != e; ++p)
-            {
-                if (cnt++ % 16 == 0)
-                {
-                    dump << std::endl;
-                }
-                dump << std::hex << std::setfill('0') << std::setw(2) << (int)*p << " ";
-            }
-            return dump.str();
-        }
     }
 }
 
diff --git a/modules/platforms/cpp/thin-client-test/CMakeLists.txt b/modules/platforms/cpp/thin-client-test/CMakeLists.txt
index 824edd0..8867f2c 100644
--- a/modules/platforms/cpp/thin-client-test/CMakeLists.txt
+++ b/modules/platforms/cpp/thin-client-test/CMakeLists.txt
@@ -28,10 +28,12 @@
 include_directories(SYSTEM ${Boost_INCLUDE_DIRS} ${JNI_INCLUDE_DIRS})
 include_directories(include)
 
-set(SOURCES src/teamcity/teamcity_boost.cpp
+set(SOURCES
+        src/teamcity/teamcity_boost.cpp
         src/teamcity/teamcity_messages.cpp
         src/cache_client_test.cpp
         src/compute_client_test.cpp
+        src/continuous_query_test.cpp
         src/test_utils.cpp
         src/ignite_client_test.cpp
         src/interop_test.cpp
@@ -55,5 +57,9 @@
 
 add_test(NAME ${TEST_TARGET} COMMAND ${TARGET} --catch_system_errors=no --log_level=all)
 
-set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT
-        IGNITE_NATIVE_TEST_CPP_THIN_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config)
+list(APPEND ENV_VARIABLES "IGNITE_NATIVE_TEST_CPP_THIN_CONFIG_PATH=${PROJECT_SOURCE_DIR}/config")
+if (${WITH_SANITIZERS})
+    list(APPEND ENV_VARIABLES ${SANITIZERS_ENV})
+endif()
+
+set_tests_properties(${TEST_TARGET} PROPERTIES ENVIRONMENT "${ENV_VARIABLES}")
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-32.xml b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-32.xml
new file mode 100644
index 0000000..1a3629e
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-32.xml
@@ -0,0 +1,48 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="cache-query-continuous-default.xml"/>
+
+    <bean parent="grid.cfg">
+        <property name="memoryConfiguration">
+            <bean class="org.apache.ignite.configuration.MemoryConfiguration">
+                <property name="systemCacheInitialSize" value="#{10 * 1024 * 1024}"/>
+                <property name="systemCacheMaxSize" value="#{40 * 1024 * 1024}"/>
+                <property name="defaultMemoryPolicyName" value="dfltPlc"/>
+
+                <property name="memoryPolicies">
+                    <list>
+                        <bean class="org.apache.ignite.configuration.MemoryPolicyConfiguration">
+                            <property name="name" value="dfltPlc"/>
+                            <property name="maxSize" value="#{100 * 1024 * 1024}"/>
+                            <property name="initialSize" value="#{10 * 1024 * 1024}"/>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-default.xml b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-default.xml
new file mode 100644
index 0000000..08e9e5f
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous-default.xml
@@ -0,0 +1,98 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <bean abstract="true" id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <property name="localHost" value="127.0.0.1"/>
+        <property name="connectorConfiguration"><null/></property>
+
+        <property name="clientConnectorConfiguration">
+            <bean class="org.apache.ignite.configuration.ClientConnectorConfiguration">
+                <property name="host" value="127.0.0.1"/>
+                <property name="port" value="11110"/>
+                <property name="portRange" value="10"/>
+            </bean>
+        </property>
+
+        <property name="cacheConfiguration">
+            <list>
+                <bean parent="cache-template">
+                    <property name="name" value="transactional_no_backup"/>
+                </bean>
+
+                <bean parent="cache-template">
+                    <property name="name" value="with_expiry"/>
+                    <property name="expiryPolicyFactory">
+                        <bean class="javax.cache.expiry.CreatedExpiryPolicy" factory-method="factoryOf">
+                            <constructor-arg>
+                                <bean class="javax.cache.expiry.Duration">
+                                    <constructor-arg value="MILLISECONDS"/>
+                                    <constructor-arg value="500"/>
+                                </bean>
+                            </constructor-arg>
+                        </bean>
+                    </property>
+                </bean>
+            </list>
+        </property>
+
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+                <property name="socketTimeout" value="300" />
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="cache-template" abstract="true" class="org.apache.ignite.configuration.CacheConfiguration">
+        <property name="cacheMode" value="PARTITIONED"/>
+        <property name="atomicityMode" value="TRANSACTIONAL"/>
+        <property name="writeSynchronizationMode" value="FULL_SYNC"/>
+        <property name="backups" value="0"/>
+        <property name="queryEntities">
+            <list>
+                <bean class="org.apache.ignite.cache.QueryEntity">
+                    <property name="keyType" value="java.lang.Integer"/>
+                    <property name="valueType" value="TestEntry"/>
+
+                    <property name="fields">
+                        <map>
+                            <entry key="value" value="java.lang.Integer"/>
+                        </map>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/config/cache-query-continuous.xml b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous.xml
new file mode 100644
index 0000000..1c4e275
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/cache-query-continuous.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <import resource="cache-query-continuous-default.xml"/>
+
+    <bean parent="grid.cfg"/>
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-32.xml b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-32.xml
new file mode 100644
index 0000000..7ea7e9f
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-32.xml
@@ -0,0 +1,48 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+    <import resource="ssl-no-client-auth-default.xml"/>
+
+    <bean parent="no-client-auth.cfg">
+        <property name="memoryConfiguration">
+            <bean class="org.apache.ignite.configuration.MemoryConfiguration">
+                <property name="systemCacheInitialSize" value="#{10 * 1024 * 1024}"/>
+                <property name="systemCacheMaxSize" value="#{40 * 1024 * 1024}"/>
+                <property name="defaultMemoryPolicyName" value="dfltPlc"/>
+
+                <property name="memoryPolicies">
+                    <list>
+                        <bean class="org.apache.ignite.configuration.MemoryPolicyConfiguration">
+                            <property name="name" value="dfltPlc"/>
+                            <property name="maxSize" value="#{100 * 1024 * 1024}"/>
+                            <property name="initialSize" value="#{10 * 1024 * 1024}"/>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-default.xml b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-default.xml
new file mode 100644
index 0000000..495a1c0
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth-default.xml
@@ -0,0 +1,84 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+
+    <!--
+        Initialize property configurer so we can reference environment variables.
+    -->
+    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
+        <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_FALLBACK"/>
+        <property name="searchSystemEnvironment" value="true"/>
+    </bean>
+  
+    <bean abstract="true" id="no-client-auth.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <property name="localHost" value="127.0.0.1"/>
+        <property name="connectorConfiguration"><null/></property>
+
+        <property name="clientConnectorConfiguration">
+            <bean class="org.apache.ignite.configuration.ClientConnectorConfiguration">
+                <property name="host" value="127.0.0.1"/>
+                <property name="port" value="11110"/>
+                <property name="portRange" value="10"/>
+                <property name="sslEnabled" value="true"/>
+                <property name="useIgniteSslContextFactory" value="false"/>
+                <property name="sslClientAuth" value="false"/>
+
+                <!-- Provide Ssl context. -->
+                <property name="sslContextFactory">
+                    <bean class="org.apache.ignite.ssl.SslContextFactory">
+                        <property name="keyStoreFilePath" value="${IGNITE_NATIVE_TEST_CPP_THIN_CONFIG_PATH}/ssl/server.jks"/>
+                        <property name="keyStorePassword" value="123456"/>
+                        <property name="trustStoreFilePath" value="${IGNITE_NATIVE_TEST_CPP_THIN_CONFIG_PATH}/ssl/trust.jks"/>
+                        <property name="trustStorePassword" value="123456"/>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+
+        <!-- Explicitly configure TCP discovery SPI to provide list of initial nodes. -->
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <!--
+                        Ignite provides several options for automatic discovery that can be used
+                        instead os static IP based discovery.
+                    -->
+                    <!-- Uncomment static IP finder to enable static-based discovery of initial nodes. -->
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+                <property name="socketTimeout" value="300" />
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth.xml b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth.xml
new file mode 100644
index 0000000..384b1e8
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/config/ssl-no-client-auth.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+    <import resource="ssl-no-client-auth-default.xml"/>
+
+    <bean parent="no-client-auth.cfg"/>
+
+</beans>
diff --git a/modules/platforms/cpp/thin-client-test/src/cache_client_test.cpp b/modules/platforms/cpp/thin-client-test/src/cache_client_test.cpp
index 9bd4d89..3840b87 100644
--- a/modules/platforms/cpp/thin-client-test/src/cache_client_test.cpp
+++ b/modules/platforms/cpp/thin-client-test/src/cache_client_test.cpp
@@ -85,14 +85,14 @@
         StartNode("node1");
         StartNode("node2");
 
-        boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
         IgniteClientConfiguration cfg;
         cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
         cfg.SetPartitionAwareness(true);
 
         IgniteClient client = IgniteClient::Start(cfg);
 
+        boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
         cache::CacheClient<KeyType, int64_t> cache =
             client.GetCache<KeyType, int64_t>("partitioned");
 
@@ -465,14 +465,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<std::string, int64_t> cache =
         client.GetCache<std::string, int64_t>("partitioned");
 
@@ -495,14 +495,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<ignite::Guid, int64_t> cache =
         client.GetCache<ignite::Guid, int64_t>("partitioned");
 
@@ -525,14 +525,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<ignite::ComplexType, int64_t> cache =
         client.GetCache<ignite::ComplexType, int64_t>("partitioned");
 
@@ -571,14 +571,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<ignite::Date, int64_t> cache =
         client.GetCache<ignite::Date, int64_t>("partitioned");
 
@@ -615,14 +615,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<ignite::Time, int64_t> cache =
         client.GetCache<ignite::Time, int64_t>("partitioned");
 
@@ -653,14 +653,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<ignite::Timestamp, int64_t> cache =
         client.GetCache<ignite::Timestamp, int64_t>("partitioned");
 
@@ -845,14 +845,14 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<std::string, int64_t> cache =
         client.CreateCache<std::string, int64_t>("defaultdynamic2");
 
@@ -877,14 +877,14 @@
     StartNode("node2");
     StartNode("node3");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112,127.0.0.1:11113");
     cfg.SetPartitionAwareness(true);
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<std::string, int64_t> cache =
         client.CreateCache<std::string, int64_t>("defaultdynamic3");
 
@@ -920,13 +920,13 @@
     StartNode("node1");
     StartNode("node2");
 
-    boost::this_thread::sleep_for(boost::chrono::seconds(2));
-
     IgniteClientConfiguration cfg;
     cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
 
     IgniteClient client = IgniteClient::Start(cfg);
 
+    boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
     cache::CacheClient<std::string, int64_t> cache =
         client.CreateCache<std::string, int64_t>("defaultdynamic4");
 
diff --git a/modules/platforms/cpp/thin-client-test/src/continuous_query_test.cpp b/modules/platforms/cpp/thin-client-test/src/continuous_query_test.cpp
new file mode 100644
index 0000000..80c0912
--- /dev/null
+++ b/modules/platforms/cpp/thin-client-test/src/continuous_query_test.cpp
@@ -0,0 +1,528 @@
+/*
+ * 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.
+ */
+
+#include <deque>
+
+#include <boost/test/unit_test.hpp>
+#include <boost/optional.hpp>
+
+#include <ignite/common/concurrent.h>
+
+#include <ignite/ignition.h>
+#include <ignite/thin/ignite_client.h>
+
+#include <test_utils.h>
+
+using namespace ignite;
+using namespace ignite::thin;
+using namespace ignite::thin::cache;
+using namespace ignite::thin::cache::event;
+using namespace ignite::thin::cache::query;
+using namespace ignite::thin::cache::query::continuous;
+
+using namespace boost::unit_test;
+
+
+/**
+ * Very simple concurrent queue implementation.
+ */
+template<typename T>
+class ConcurrentQueue
+{
+public:
+    /**
+     * Constructor.
+     */
+    ConcurrentQueue()
+    {
+        // No-op.
+    }
+
+    /**
+     * Push next element to queue.
+     *
+     * @param val Value to push.
+     */
+    void Push(const T& val)
+    {
+        common::concurrent::CsLockGuard guard(mutex);
+
+        queue.push_back(val);
+
+        cv.NotifyOne();
+    }
+
+    /**
+     * Pull element from the queue with the specified timeout.
+     *
+     * @param val Value is placed there on success.
+     * @param timeout Timeout in ms.
+     * @return True on success and false on timeout.
+     */
+    bool Pull(T& val, int32_t timeout)
+    {
+        common::concurrent::CsLockGuard guard(mutex);
+
+        if (queue.empty())
+        {
+            bool notified = cv.WaitFor(mutex, timeout);
+
+            if (!notified)
+                return false;
+        }
+
+        assert(!queue.empty());
+
+        val = queue.front();
+
+        queue.pop_front();
+
+        return true;
+    }
+
+private:
+    /** Mutex. */
+    common::concurrent::CriticalSection mutex;
+
+    /** Condition variable. */
+    common::concurrent::ConditionVariable cv;
+
+    /** Queue. */
+    std::deque<T> queue;
+};
+
+/**
+ * Test listener class. Stores events it has been notified about in concurrent
+ * queue so they can be checked later.
+ */
+template<typename K, typename V>
+class Listener : public CacheEntryEventListener<K, V>
+{
+public:
+    enum { DEFAULT_WAIT_TIMEOUT = 1000 };
+
+    /**
+     * Default constructor.
+     */
+    Listener() :
+        disconnected(false),
+        handlingDelay(0)
+    {
+        // No-op.
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param handlingDelay Handling delay.
+     */
+    Listener(int32_t handlingDelay) :
+        disconnected(false),
+        handlingDelay(handlingDelay)
+    {
+        // No-op.
+    }
+
+    /**
+     * Event callback.
+     *
+     * @param evts Events.
+     * @param num Events number.
+     */
+    virtual void OnEvent(const CacheEntryEvent<K, V>* evts, uint32_t num)
+    {
+        for (uint32_t i = 0; i < num; ++i)
+        {
+            if (handlingDelay)
+                boost::this_thread::sleep_for(boost::chrono::milliseconds(handlingDelay));
+
+            eventQueue.Push(evts[i]);
+        }
+    }
+
+    /**
+     * Disconnected callback.
+     *
+     * Called if channel was disconnected. This also means that continuous query was closed and no more
+     * events will be provided for this listener.
+     */
+    virtual void OnDisconnected()
+    {
+        common::concurrent::CsLockGuard guard(disconnectedMutex);
+
+        disconnected = true;
+        disconnectedCv.NotifyAll();
+    }
+
+    /**
+     * Check that next received event contains specific values.
+     *
+     * @param key Key.
+     * @param oldVal Old value.
+     * @param val Current value.
+     * @param eType Event type.
+     */
+    void CheckNextEvent(const K& key, boost::optional<V> oldVal, boost::optional<V> val, CacheEntryEventType::Type eType)
+    {
+        CacheEntryEvent<K, V> event;
+        bool success = eventQueue.Pull(event, DEFAULT_WAIT_TIMEOUT);
+
+        BOOST_REQUIRE(success);
+
+        BOOST_CHECK_EQUAL(event.GetKey(), key);
+        BOOST_CHECK_EQUAL(event.HasOldValue(), oldVal.is_initialized());
+        BOOST_CHECK_EQUAL(event.HasValue(), val.is_initialized());
+        BOOST_CHECK_EQUAL(event.GetEventType(), eType);
+
+        if (oldVal && event.HasOldValue())
+            BOOST_CHECK_EQUAL(event.GetOldValue().value, oldVal->value);
+
+        if (val && event.HasValue())
+            BOOST_CHECK_EQUAL(event.GetValue().value, val->value);
+    }
+
+    /**
+     * Check that there is no nex event in specified period of time.
+     *
+     * @param millis Time span in milliseconds.
+     */
+    void CheckNoEvent(int32_t millis = DEFAULT_WAIT_TIMEOUT)
+    {
+        CacheEntryEvent<K, V> event;
+        bool success = eventQueue.Pull(event, millis);
+
+        BOOST_CHECK(!success);
+    }
+
+    /**
+     * Check that next event is the cache entry expiry event.
+     *
+     * @param key Key.
+     */
+    void CheckExpired(const K& key)
+    {
+        CacheEntryEvent<K, V> event;
+        bool success = eventQueue.Pull(event, DEFAULT_WAIT_TIMEOUT);
+
+        BOOST_CHECK(success);
+
+        BOOST_CHECK_EQUAL(event.GetKey(), key);
+        BOOST_CHECK_EQUAL(event.GetEventType(), CacheEntryEventType::EXPIRED);
+    }
+
+    /**
+     * Make sure that channel is disconnected within specified time.
+     *
+     * @param millis Time span in milliseconds.
+     */
+    void CheckDisconnected(int32_t millis = DEFAULT_WAIT_TIMEOUT)
+    {
+        common::concurrent::CsLockGuard guard(disconnectedMutex);
+
+        if (disconnected)
+            return;
+
+        disconnectedCv.WaitFor(disconnectedMutex, millis);
+        BOOST_CHECK(disconnected);
+    }
+
+private:
+    /** Disconnected Mutex. */
+    common::concurrent::CriticalSection disconnectedMutex;
+
+    /** Disconnected Condition variable. */
+    common::concurrent::ConditionVariable disconnectedCv;
+
+    /** Disconnected flag. */
+    bool disconnected;
+
+    /** Handling delay. */
+    int32_t handlingDelay;
+
+    /** Events queue. */
+    ConcurrentQueue< CacheEntryEvent<K, V> > eventQueue;
+};
+
+/*
+ * Test entry.
+ */
+struct TestEntry
+{
+    /*
+     * Default constructor.
+     */
+    TestEntry() : value(0)
+    {
+        // No-op.
+    }
+
+    /*
+     * Constructor.
+     */
+    explicit TestEntry(int32_t val) : value(val)
+    {
+        // No-op.
+    }
+
+    /* Value */
+    int32_t value;
+};
+
+namespace ignite
+{
+    namespace binary
+    {
+        template<>
+        struct BinaryType<TestEntry> : BinaryTypeDefaultAll<TestEntry>
+        {
+            static void GetTypeName(std::string& dst)
+            {
+                dst = "TestEntry";
+            }
+
+            static void Write(BinaryWriter& writer, const TestEntry& obj)
+            {
+                writer.WriteInt32("value", obj.value);
+            }
+
+            static void Read(BinaryReader& reader, TestEntry& dst)
+            {
+                dst.value = reader.ReadInt32("value");
+            }
+        };
+    }
+}
+
+/**
+ * Test setup fixture.
+ */
+class ContinuousQueryTestSuiteFixture
+{
+public:
+    /**
+     * Constructor.
+     */
+    ContinuousQueryTestSuiteFixture() :
+        node(ignite_test::StartCrossPlatformServerNode("cache-query-continuous.xml", "node-01")),
+        client(),
+        cache()
+    {
+        client = StartClient();
+        cache = GetDefaultCache(client);
+    }
+
+    /**
+     * Destructor.
+     */
+    ~ContinuousQueryTestSuiteFixture()
+    {
+        Ignition::StopAll(false);
+
+        node = Ignite();
+    }
+
+    /**
+     * Start new client.
+     */
+    static IgniteClient StartClient()
+    {
+        IgniteClientConfiguration cfg;
+        cfg.SetEndPoints("127.0.0.1:11110");
+
+        return IgniteClient::Start(cfg);
+    }
+
+    /**
+     * Get test cache using client.
+     *
+     * @param client Client to use.
+     */
+    static CacheClient<int32_t, TestEntry> GetDefaultCache(IgniteClient& client)
+    {
+        return client.GetOrCreateCache<int32_t, TestEntry>("transactional_no_backup");
+    }
+
+    /**
+     * Get cache with configured expiry policy using client.
+     *
+     * @param client Client to use.
+     */
+    static CacheClient<int32_t, TestEntry> GetExpiryCache(IgniteClient& client)
+    {
+        return client.GetOrCreateCache<int32_t, TestEntry>("with_expiry");
+    }
+
+protected:
+    /** Node. */
+    Ignite node;
+
+    /** Client. */
+    IgniteClient client;
+
+    /** Cache client. */
+    CacheClient<int32_t, TestEntry> cache;
+};
+
+void CheckEvents(CacheClient<int32_t, TestEntry>& cache, Listener<int32_t, TestEntry>& listener)
+{
+    cache.Put(1, TestEntry(10));
+    listener.CheckNextEvent(1, boost::none, TestEntry(10), CacheEntryEventType::CREATED);
+
+    cache.Put(1, TestEntry(20));
+    listener.CheckNextEvent(1, TestEntry(10), TestEntry(20), CacheEntryEventType::UPDATED);
+
+    cache.Put(2, TestEntry(20));
+    listener.CheckNextEvent(2, boost::none, TestEntry(20), CacheEntryEventType::CREATED);
+
+    cache.Remove(1);
+    listener.CheckNextEvent(1, TestEntry(20), TestEntry(20), CacheEntryEventType::REMOVED);
+}
+
+BOOST_FIXTURE_TEST_SUITE(ContinuousQueryTestSuite, ContinuousQueryTestSuiteFixture)
+
+BOOST_AUTO_TEST_CASE(TestBasic)
+{
+    Listener<int32_t, TestEntry> listener;
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    CheckEvents(cache, listener);
+}
+
+BOOST_AUTO_TEST_CASE(TestExpiredQuery)
+{
+    Listener<int32_t, TestEntry> listener;
+    ContinuousQueryHandleClient handle;
+
+    {
+        // Query scope.
+        ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+
+        handle = cache.QueryContinuous(qry);
+    }
+
+    // Query is destroyed here.
+
+    CheckEvents(cache, listener);
+}
+
+BOOST_AUTO_TEST_CASE(TestExpiredEventsReceived)
+{
+    cache = GetExpiryCache(client);
+
+    Listener<int32_t, TestEntry> listener;
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+    qry.SetIncludeExpired(true);
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    cache.Put(1, TestEntry(10));
+    listener.CheckNextEvent(1, boost::none, TestEntry(10), CacheEntryEventType::CREATED);
+    listener.CheckNoEvent(100);
+    listener.CheckExpired(1);
+}
+
+BOOST_AUTO_TEST_CASE(TestExpiredEventsNotReceived)
+{
+    cache = GetExpiryCache(client);
+
+    Listener<int32_t, TestEntry> listener;
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+    qry.SetIncludeExpired(false);
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    cache.Put(1, TestEntry(10));
+    listener.CheckNextEvent(1, boost::none, TestEntry(10), CacheEntryEventType::CREATED);
+    listener.CheckNoEvent();
+}
+
+BOOST_AUTO_TEST_CASE(TestGetSetBufferSize)
+{
+    typedef ContinuousQueryClient<int32_t, TestEntry> QueryType;
+    Listener<int32_t, TestEntry> listener;
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+
+    BOOST_CHECK_EQUAL(qry.GetBufferSize(), (int32_t) QueryType::DEFAULT_BUFFER_SIZE);
+
+    qry.SetBufferSize(2 * QueryType::DEFAULT_BUFFER_SIZE);
+
+    BOOST_CHECK_EQUAL(qry.GetBufferSize(), 2 * QueryType::DEFAULT_BUFFER_SIZE);
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    BOOST_CHECK_EQUAL(qry.GetBufferSize(), 2 * QueryType::DEFAULT_BUFFER_SIZE);
+
+    CheckEvents(cache, listener);
+}
+
+BOOST_AUTO_TEST_CASE(TestGetSetTimeInterval)
+{
+    typedef ContinuousQueryClient<int32_t, TestEntry> QueryType;
+    Listener<int32_t, TestEntry> listener;
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReference(listener));
+
+    qry.SetBufferSize(10);
+
+    BOOST_CHECK_EQUAL(qry.GetTimeInterval(), static_cast<int>(QueryType::DEFAULT_TIME_INTERVAL));
+
+    qry.SetTimeInterval(500);
+
+    BOOST_CHECK_EQUAL(qry.GetTimeInterval(), 500);
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    BOOST_CHECK_EQUAL(qry.GetTimeInterval(), 500);
+
+    CheckEvents(cache, listener);
+}
+
+BOOST_AUTO_TEST_CASE(TestPublicPrivateConstantsConsistence)
+{
+    typedef ContinuousQueryClient<int32_t, TestEntry> QueryType;
+    typedef impl::cache::query::continuous::ContinuousQueryImpl<int, TestEntry> QueryImplType;
+    
+    BOOST_CHECK_EQUAL(static_cast<int>(QueryImplType::DEFAULT_TIME_INTERVAL),
+        static_cast<int>(QueryType::DEFAULT_TIME_INTERVAL));
+
+    BOOST_CHECK_EQUAL(static_cast<int>(QueryImplType::DEFAULT_BUFFER_SIZE),
+        static_cast<int>(QueryType::DEFAULT_BUFFER_SIZE));
+}
+
+BOOST_AUTO_TEST_CASE(TestLongEventsProcessingDisconnect)
+{
+    boost::shared_ptr< Listener<int32_t, TestEntry> > listener(new Listener<int32_t, TestEntry>(200));
+
+    ContinuousQueryClient<int32_t, TestEntry> qry(MakeReferenceFromSmartPointer(listener));
+
+    ContinuousQueryHandleClient handle = cache.QueryContinuous(qry);
+
+    for (int32_t i = 0; i < 20; ++i)
+        cache.Put(i, TestEntry(i * 10));
+
+    Ignition::Stop(node.GetName(), true);
+
+    listener->CheckDisconnected();
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/thin-client-test/src/ignite_client_test.cpp b/modules/platforms/cpp/thin-client-test/src/ignite_client_test.cpp
index 7476305..786b883 100644
--- a/modules/platforms/cpp/thin-client-test/src/ignite_client_test.cpp
+++ b/modules/platforms/cpp/thin-client-test/src/ignite_client_test.cpp
@@ -66,6 +66,9 @@
         IgniteClient client = IgniteClient::Start(cfg);
 
         BOOST_CHECK(WaitForConnections(expect));
+
+        boost::this_thread::sleep_for(boost::chrono::seconds(2));
+
         BOOST_CHECK_EQUAL(GetActiveConnections(), expect);
     }
 
@@ -109,30 +112,34 @@
 
 BOOST_AUTO_TEST_CASE(IgniteClientConnection)
 {
-    ignite::Ignite serverNode = ignite_test::StartCrossPlatformServerNode("cache.xml", "ServerNode");
+    ignite::Ignite serverNode = StartNodeWithLog("0");
 
     IgniteClientConfiguration cfg;
 
     cfg.SetEndPoints("127.0.0.1:11110");
 
-    IgniteClient::Start(cfg);
+    IgniteClient client = IgniteClient::Start(cfg);
+
+    BOOST_CHECK(WaitForConnections(1));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 1);
 }
 
 BOOST_AUTO_TEST_CASE(IgniteClientConnectionFailover)
 {
-    ignite::Ignite serverNode = ignite_test::StartCrossPlatformServerNode("cache.xml", "ServerNode");
+    ignite::Ignite serverNode = StartNodeWithLog("0");
 
     IgniteClientConfiguration cfg;
 
     cfg.SetEndPoints("127.0.0.1:11109..11111");
 
-    IgniteClient::Start(cfg);
+    IgniteClient client = IgniteClient::Start(cfg);
+
+    BOOST_CHECK(WaitForConnections(1));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 1);
 }
 
 BOOST_AUTO_TEST_CASE(IgniteClientConnectionLimit)
 {
-    ignite::common::DeletePath("logs");
-
     ignite::Ignite serverNode0 = StartNodeWithLog("0");
     ignite::Ignite serverNode1 = StartNodeWithLog("1");
     ignite::Ignite serverNode2 = StartNodeWithLog("2");
@@ -149,4 +156,41 @@
     CheckConnectionsNum(cfg, 100500, 3);
 }
 
+BOOST_AUTO_TEST_CASE(IgniteClientReconnect)
+{
+    ignite::Ignite serverNode0 = StartNodeWithLog("0");
+    ignite::Ignite serverNode1 = StartNodeWithLog("1");
+
+    IgniteClientConfiguration cfg;
+
+    cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11111,127.0.0.1:11112");
+
+    IgniteClient client = IgniteClient::Start(cfg);
+
+    BOOST_CHECK(WaitForConnections(2));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 2);
+
+    ignite::Ignite serverNode2 = StartNodeWithLog("2");
+
+    BOOST_CHECK(WaitForConnections(3));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 3);
+
+    ignite::Ignition::Stop(serverNode1.GetName(), true);
+
+    BOOST_CHECK(WaitForConnections(2));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 2);
+
+    serverNode1 = StartNodeWithLog("1");
+
+    BOOST_CHECK(WaitForConnections(3, 20000));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 3);
+
+    ignite::Ignition::StopAll(true);
+
+    BOOST_CHECK(WaitForConnections(0));
+    BOOST_CHECK_EQUAL(GetActiveConnections(), 0);
+
+    BOOST_REQUIRE_THROW((client.GetOrCreateCache<int, int>("test")), ignite::IgniteError);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/thin-client-test/src/sql_fields_query_test.cpp b/modules/platforms/cpp/thin-client-test/src/sql_fields_query_test.cpp
index cbc1a4f..838e830 100644
--- a/modules/platforms/cpp/thin-client-test/src/sql_fields_query_test.cpp
+++ b/modules/platforms/cpp/thin-client-test/src/sql_fields_query_test.cpp
@@ -210,6 +210,7 @@
  *
  * @param seed Seed to generate value.
  */
+IGNORE_SIGNED_OVERFLOW
 ignite::TestType MakeCustomTestValue(int32_t seed)
 {
     ignite::TestType val;
diff --git a/modules/platforms/cpp/thin-client-test/src/ssl_test.cpp b/modules/platforms/cpp/thin-client-test/src/ssl_test.cpp
index 845360b..10770e4 100644
--- a/modules/platforms/cpp/thin-client-test/src/ssl_test.cpp
+++ b/modules/platforms/cpp/thin-client-test/src/ssl_test.cpp
@@ -17,6 +17,8 @@
 
 #include <boost/test/unit_test.hpp>
 
+#include <ignite/common/utils.h>
+
 #include <ignite/ignition.h>
 
 #include <ignite/thin/ignite_client_configuration.h>
@@ -30,14 +32,19 @@
 class SslTestSuiteFixture
 {
 public:
-    ignite::Ignite StartSslNode()
+    ignite::Ignite StartSslNode(const std::string& name = "ServerNode")
     {
-        return ignite_test::StartCrossPlatformServerNode("ssl.xml", "ServerNode");
+        return ignite_test::StartCrossPlatformServerNode("ssl.xml", name.c_str());
     }
     
-    ignite::Ignite StartNonSslNode()
+    ignite::Ignite StartNonSslNode(const std::string& name = "ServerNode")
     {
-        return ignite_test::StartCrossPlatformServerNode("non-ssl.xml", "ServerNode");
+        return ignite_test::StartCrossPlatformServerNode("non-ssl.xml", name.c_str());
+    }
+
+    ignite::Ignite StartSslNoClientAuthNode(const std::string& name = "ServerNode")
+    {
+        return ignite_test::StartCrossPlatformServerNode("ssl-no-client-auth.xml", name.c_str());
     }
 
     SslTestSuiteFixture()
@@ -124,4 +131,197 @@
     BOOST_CHECK_THROW(IgniteClient::Start(cfg), ignite::IgniteError);
 }
 
+BOOST_AUTO_TEST_CASE(SslCacheClientPutAllGetAll)
+{
+    StartSslNode("node1");
+    StartSslNode("node2");
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslCertFile(GetConfigFile("client_full.pem"));
+    cfg.SetSslKeyFile(GetConfigFile("client_full.pem"));
+    cfg.SetSslCaFile(GetConfigFile("ca.pem"));
+
+    IgniteClient client = IgniteClient::Start(cfg);
+
+    cache::CacheClient<int32_t, std::string> cache =
+            client.CreateCache<int32_t, std::string>("test");
+
+    enum { BATCH_SIZE = 20000 };
+
+    std::map<int32_t, std::string> values;
+    std::set<int32_t> keys;
+
+    for (int32_t j = 0; j < BATCH_SIZE; ++j)
+    {
+        int32_t key = BATCH_SIZE + j;
+
+        values[key] = "value_" + ignite::common::LexicalCast<std::string>(key);
+        keys.insert(key);
+    }
+
+    cache.PutAll(values);
+
+    std::map<int32_t, std::string> retrieved;
+    cache.GetAll(keys, retrieved);
+
+    BOOST_REQUIRE(values == retrieved);
+}
+
+BOOST_AUTO_TEST_CASE(SslCacheClientPutGet)
+{
+    StartSslNode("node1");
+    StartSslNode("node2");
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110,127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslCertFile(GetConfigFile("client_full.pem"));
+    cfg.SetSslKeyFile(GetConfigFile("client_full.pem"));
+    cfg.SetSslCaFile(GetConfigFile("ca.pem"));
+
+    IgniteClient client = IgniteClient::Start(cfg);
+
+    cache::CacheClient<int32_t, std::string> cache =
+            client.CreateCache<int32_t, std::string>("test");
+
+    enum { OPS_NUM = 100 };
+    for (int32_t j = 0; j < OPS_NUM; ++j)
+    {
+        int32_t key = OPS_NUM + j;
+        std::string value = "value_" + ignite::common::LexicalCast<std::string>(key);
+
+        cache.Put(key, value);
+        std::string retrieved = cache.Get(key);
+
+        BOOST_REQUIRE_EQUAL(value, retrieved);
+    }
+}
+
+BOOST_AUTO_TEST_CASE(SslConnectionNoCerts)
+{
+    StartSslNoClientAuthNode();
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslCaFile(GetConfigFile("ca.pem"));
+
+    IgniteClient client = IgniteClient::Start(cfg);
+}
+
+/**
+ * Check whether error is "file not exists".
+ *
+ * @param err Error to check
+ * @return @true is Error is of expected kind.
+ */
+bool IsNonExisting(const ignite::IgniteError& err)
+{
+    if (err.GetCode() != ignite::IgniteError::IGNITE_ERR_SECURE_CONNECTION_FAILURE)
+        return false;
+
+    std::string msg(err.GetText());
+
+    if (msg.find("error:02001002") == std::string::npos &&
+        msg.find("error:2006D080") == std::string::npos)
+        return false;
+
+    if (msg.find("No such file or directory") == std::string::npos &&
+        msg.find("no such file") == std::string::npos)
+        return false;
+
+    return true;
+}
+
+/**
+ * Check whether error is "CA file not exists".
+ *
+ * @param err Error to check
+ * @return @true is Error is of expected kind.
+ */
+bool IsNonExistingCa(const ignite::IgniteError& err)
+{
+    if (!IsNonExisting(err))
+        return false;
+
+    std::string msg(err.GetText());
+    return msg.find("Can not set Certificate Authority path for secure connection") != std::string::npos;
+}
+
+BOOST_AUTO_TEST_CASE(SslConnectionErrorNonExistingCa)
+{
+    StartSslNoClientAuthNode();
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslCaFile(GetConfigFile("non_existing_ca.pem"));
+
+    BOOST_CHECK_EXCEPTION(IgniteClient::Start(cfg), ignite::IgniteError, IsNonExistingCa);
+}
+
+/**
+ * Check whether error is "private key file not exists".
+ *
+ * @param err Error to check
+ * @return @true is Error is of expected kind.
+ */
+bool IsNonExistingKey(const ignite::IgniteError& err)
+{
+    std::cout << err.GetText() << std::endl;
+    if (!IsNonExisting(err))
+        return false;
+
+    std::string msg(err.GetText());
+    return msg.find("Can not set private key file for secure connection") != std::string::npos;
+}
+
+BOOST_AUTO_TEST_CASE(SslConnectionErrorNonExistingKey)
+{
+    StartSslNoClientAuthNode();
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslKeyFile(GetConfigFile("non_existing_key.pem"));
+
+    BOOST_CHECK_EXCEPTION(IgniteClient::Start(cfg), ignite::IgniteError, IsNonExistingKey);
+}
+
+
+/**
+ * Check whether error is "certificate file not exists".
+ *
+ * @param err Error to check
+ * @return @true is Error is of expected kind.
+ */
+bool IsNonExistingCert(const ignite::IgniteError& err)
+{
+    if (!IsNonExisting(err))
+        return false;
+
+    std::string msg(err.GetText());
+    return msg.find("Can not set client certificate file for secure connection") != std::string::npos;
+}
+
+BOOST_AUTO_TEST_CASE(SslConnectionErrorNonExistingCert)
+{
+    StartSslNoClientAuthNode();
+
+    IgniteClientConfiguration cfg;
+    cfg.SetEndPoints("127.0.0.1:11110");
+
+    cfg.SetSslMode(SslMode::REQUIRE);
+    cfg.SetSslCertFile(GetConfigFile("non_existing_Cert.pem"));
+
+    BOOST_CHECK_EXCEPTION(IgniteClient::Start(cfg), ignite::IgniteError, IsNonExistingCert);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/platforms/cpp/thin-client-test/src/test_utils.cpp b/modules/platforms/cpp/thin-client-test/src/test_utils.cpp
index fd179d2..0c56d70 100644
--- a/modules/platforms/cpp/thin-client-test/src/test_utils.cpp
+++ b/modules/platforms/cpp/thin-client-test/src/test_utils.cpp
@@ -71,7 +71,7 @@
         cfg.jvmOpts.push_back("-DIGNITE_LOG_CLASSPATH_CONTENT_ON_STARTUP=false");
         cfg.jvmOpts.push_back("-Duser.language=en");
         // Un-comment to debug SSL
-        //cfg.jvmOpts.push_back("-Djavax.net.debug=ssl");
+        // cfg.jvmOpts.push_back("-Djavax.net.debug=ssl");
 
         cfg.igniteHome = jni::ResolveIgniteHome();
         cfg.jvmClassPath = jni::CreateIgniteHomeClasspath(cfg.igniteHome, true);
diff --git a/modules/platforms/cpp/thin-client/CMakeLists.txt b/modules/platforms/cpp/thin-client/CMakeLists.txt
index 29dadca..20d52ec 100644
--- a/modules/platforms/cpp/thin-client/CMakeLists.txt
+++ b/modules/platforms/cpp/thin-client/CMakeLists.txt
@@ -30,6 +30,7 @@
         src/impl/affinity/affinity_topology_version.cpp
         src/impl/affinity/affinity_assignment.cpp
         src/impl/affinity/affinity_manager.cpp
+        src/impl/cache/query/continuous/continuous_query_notification_handler.cpp
         src/impl/remote_type_updater.cpp
         src/impl/message.cpp
         src/impl/cache/cache_client_proxy.cpp
diff --git a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/cache_client_proxy.h b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/cache_client_proxy.h
index da5f42b..01ee395 100644
--- a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/cache_client_proxy.h
+++ b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/cache_client_proxy.h
@@ -23,6 +23,8 @@
 #include <ignite/thin/cache/query/query_fields_cursor.h>
 #include <ignite/thin/cache/query/query_sql_fields.h>
 
+#include <ignite/impl/thin/cache/continuous/continuous_query_client_holder.h>
+
 namespace ignite
 {
     namespace impl
@@ -298,6 +300,15 @@
                             const ignite::thin::cache::query::SqlFieldsQuery& qry);
 
                     /**
+                     * Starts the continuous query execution
+                     *
+                     * @param continuousQuery Continuous query.
+                     * @return Query handle. Once all instances are destroyed query execution stopped.
+                     */
+                    ignite::thin::cache::query::continuous::ContinuousQueryHandleClient QueryContinuous(
+                            const query::continuous::SP_ContinuousQueryClientHolderBase& continuousQuery);
+
+                    /**
                      * Get from CacheClient.
                      * Use for testing purposes only.
                      */
diff --git a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/continuous/continuous_query_client_holder.h b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/continuous/continuous_query_client_holder.h
new file mode 100644
index 0000000..13dee90
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/continuous/continuous_query_client_holder.h
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
+#define _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
+
+#include <ignite/common/concurrent.h>
+
+#include <ignite/thin/cache/query/continuous/continuous_query_client.h>
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        /**
+                         * Continuous query client holder.
+                         */
+                        class ContinuousQueryClientHolderBase
+                        {
+                        public:
+                            /**
+                             * Destructor.
+                             */
+                            virtual ~ContinuousQueryClientHolderBase()
+                            {
+                                // No-op.
+                            }
+
+                            /**
+                             * Read and process cache events.
+                             *
+                             * @param reader Reader to use.
+                             */
+                            virtual void ReadAndProcessEvents(ignite::binary::BinaryRawReader& reader) = 0;
+
+                            /**
+                             * Get buffer size.
+                             *
+                             * @return Buffer size.
+                             */
+                            virtual int32_t GetBufferSize() const = 0;
+
+                            /**
+                             * Get time interval.
+                             *
+                             * @return Time interval.
+                             */
+                            virtual int64_t GetTimeInterval() const = 0;
+
+                            /**
+                             * Gets a value indicating whether to notify about Expired events.
+                             *
+                             * @return Flag value.
+                             */
+                            virtual bool GetIncludeExpired() const = 0;
+
+                            /**
+                             * Disconnected callback.
+                             *
+                             * Called if channel was disconnected.
+                             */
+                            virtual void OnDisconnected() = 0;
+                        };
+
+                        /** Shared pointer to ContinuousQueryClientHolderBase. */
+                        typedef common::concurrent::SharedPointer<ContinuousQueryClientHolderBase> SP_ContinuousQueryClientHolderBase;
+
+                        /**
+                         * Continuous query client holder.
+                         */
+                        template<typename K, typename V>
+                        class ContinuousQueryClientHolder : public ContinuousQueryClientHolderBase
+                        {
+                        public:
+                            /** Wrapped continuous query type. */
+                            typedef ignite::thin::cache::query::continuous::ContinuousQueryClient<K, V> ContinuousQuery;
+
+                            /**
+                             * Constructor.
+                             *
+                             * @param continuousQuery
+                             */
+                            ContinuousQueryClientHolder(const ContinuousQuery& continuousQuery) :
+                                continuousQuery(continuousQuery)
+                            {
+                                // No-op.
+                            }
+
+                            /**
+                             * Read and process cache events.
+                             *
+                             * @param reader Reader to use.
+                             */
+                            virtual void ReadAndProcessEvents(ignite::binary::BinaryRawReader& reader)
+                            {
+                                // Number of events.
+                                int32_t cnt = reader.ReadInt32();
+
+                                // Storing events here.
+                                std::vector< ignite::thin::cache::CacheEntryEvent<K, V> > events;
+                                events.resize(cnt);
+
+                                for (int32_t i = 0; i < cnt; ++i)
+                                    events[i].Read(reader);
+
+                                continuousQuery.GetListener().OnEvent(events.data(), cnt);
+                            }
+
+                            /**
+                             * Get buffer size.
+                             *
+                             * @return Buffer size.
+                             */
+                            virtual int32_t GetBufferSize() const
+                            {
+                                return continuousQuery.GetBufferSize();
+                            }
+
+                            /**
+                             * Get time interval.
+                             *
+                             * @return Time interval.
+                             */
+                            virtual int64_t GetTimeInterval() const
+                            {
+                                return continuousQuery.GetTimeInterval();
+                            }
+
+                            /**
+                             * Gets a value indicating whether to notify about Expired events.
+                             *
+                             * @return Flag value.
+                             */
+                            virtual bool GetIncludeExpired() const
+                            {
+                                return continuousQuery.GetIncludeExpired();
+                            }
+
+                            /**
+                             * Disconnected callback.
+                             *
+                             * Called if channel was disconnected.
+                             */
+                            virtual void OnDisconnected()
+                            {
+                                continuousQuery.GetListener().OnDisconnected();
+                            }
+
+                        private:
+                            /** Continuous query. */
+                            ContinuousQuery continuousQuery;
+                        };
+                    }
+                }
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_cursor_impl.h b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_cursor_impl.h
deleted file mode 100644
index 13af4cb..0000000
--- a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_cursor_impl.h
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.
- */
-#ifndef _IGNITE_IMPL_THIN_CACHE_QUERY_QUERY_FIELDS_CURSOR_IMPL
-#define _IGNITE_IMPL_THIN_CACHE_QUERY_QUERY_FIELDS_CURSOR_IMPL
-
-#include <ignite/ignite_error.h>
-
-#include "ignite/impl/ignite_environment.h"
-#include "ignite/impl/operations.h"
-#include "ignite/impl/cache/query/query_batch.h"
-
-namespace ignite
-{
-    namespace impl
-    {
-        namespace thin
-        {
-            namespace cache
-            {
-                namespace query
-                {
-                    class QueryFieldsRowImpl;
-
-                    /**
-                     * Query cursor implementation.
-                     */
-                    class IGNITE_IMPORT_EXPORT QueryCursorImpl
-                    {
-                    public:
-                        /**
-                         * Constructor.
-                         *
-                         * @param env Environment.
-                         * @param javaRef Java reference.
-                         */
-                        QueryCursorImpl(ignite::common::concurrent::SharedPointer<IgniteEnvironment> env, jobject javaRef);
-
-                        /**
-                         * Destructor.
-                         */
-                        ~QueryCursorImpl();
-
-                        /**
-                         * Check whether next result exists.
-                         *
-                         * @param err Error.
-                         * @return True if exists.
-                         */
-                        bool HasNext(IgniteError& err);
-
-                        /**
-                         * Get next object.
-                         *
-                         * @param op Operation.
-                         * @param err Error.
-                         */
-                        void GetNext(OutputOperation& op, IgniteError& err);
-
-                        /**
-                         * Get next row.
-                         *
-                         * @param err Error.
-                         * @return Output row.
-                         */
-                        QueryFieldsRowImpl* GetNextRow(IgniteError& err);
-
-                        /**
-                         * Get all cursor entries.
-                         *
-                         * @param op Operation.
-                         * @param err Error.
-                         */
-                        void GetAll(OutputOperation& op, IgniteError& err);
-
-                        /**
-                         * Get all cursor entries.
-                         *
-                         * @param op Operation.
-                         */
-                        void GetAll(OutputOperation& op);
-
-                    private:
-                        /** Environment. */
-                        ignite::common::concurrent::SharedPointer<impl::IgniteEnvironment> env;
-
-                        /** Handle to Java object. */
-                        jobject javaRef;
-
-                        /** Current result batch. */
-                        QueryBatch* batch;
-
-                        /** Whether cursor has no more elements available. */
-                        bool endReached;
-
-                        /** Whether iteration methods were called. */
-                        bool iterCalled;
-
-                        /** Whether GetAll() method was called. */
-                        bool getAllCalled;
-
-                        IGNITE_NO_COPY_ASSIGNMENT(QueryCursorImpl);
-
-                        /**
-                         * Create Java-side iterator if needed.
-                         *
-                         * @param err Error.
-                         * @return True in case of success, false if an error is thrown.
-                         */
-                        bool CreateIteratorIfNeeded(IgniteError& err);
-
-                       /**
-                         * Get next result batch if update is needed.
-                         *
-                         * @param err Error.
-                         * @return True if operation has been successful.
-                         */
-                        bool GetNextBatchIfNeeded(IgniteError& err);
-
-                        /**
-                         * Check whether Java-side iterator has next element.
-                         *
-                         * @param err Error.
-                         * @return True if the next element is available.
-                         */
-                        bool IteratorHasNext(IgniteError& err);
-                    };
-                }
-            }
-        }
-    }
-}
-
-#endif //_IGNITE_IMPL_THIN_CACHE_QUERY_QUERY_FIELDS_CURSOR_IMPL
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_row_impl.h b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_row_impl.h
deleted file mode 100644
index 3c08901..0000000
--- a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/cache/query/query_fields_row_impl.h
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.
- */
-
-#ifndef _IGNITE_IMPL_CACHE_QUERY_CACHE_QUERY_FIELDS_ROW_IMPL
-#define _IGNITE_IMPL_CACHE_QUERY_CACHE_QUERY_FIELDS_ROW_IMPL
-
-#include <ignite/common/concurrent.h>
-#include <ignite/ignite_error.h>
-
-namespace ignite
-{
-    namespace impl
-    {
-        namespace cache
-        {
-            namespace query
-            {
-                /**
-                 * Query fields cursor implementation.
-                 */
-                class IGNITE_IMPORT_EXPORT QueryFieldsRowImpl
-                {
-                public:
-                    typedef common::concurrent::SharedPointer<interop::InteropMemory> SP_InteropMemory;
-
-                    /**
-                     * Constructor.
-                     *
-                     * @param mem Memory containig row data.
-                     */
-                    QueryFieldsRowImpl(SP_InteropMemory mem, int32_t rowBegin, int32_t columnNum) :
-                        mem(mem),
-                        stream(mem.Get()),
-                        reader(&stream),
-                        columnNum(columnNum),
-                        processed(0)
-                    {
-                        stream.Position(rowBegin);
-                    }
-
-                    /**
-                     * Check whether next entry exists.
-                     *
-                     * @return True if next entry exists.
-                     */
-                    bool HasNext()
-                    {
-                        IgniteError err;
-
-                        bool res = HasNext(err);
-
-                        IgniteError::ThrowIfNeeded(err);
-
-                        return res;
-                    }
-
-                    /**
-                     * Check whether next entry exists.
-                     *
-                     * @param err Error.
-                     * @return True if next entry exists.
-                     */
-                    bool HasNext(IgniteError& err)
-                    {
-                        if (IsValid())
-                            return processed < columnNum;
-                        else
-                        {
-                            err = IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                                "Instance is not usable (did you check for error?).");
-
-                            return false;
-                        }
-                    }
-
-                    /**
-                     * Get next entry.
-                     *
-                     * @return Next entry.
-                     */
-                    template<typename T>
-                    T GetNext()
-                    {
-                        IgniteError err;
-
-                        QueryFieldsRowImpl res = GetNext<T>(err);
-
-                        IgniteError::ThrowIfNeeded(err);
-
-                        return res;
-                    }
-
-                    /**
-                     * Get next entry.
-                     *
-                     * @param err Error.
-                     * @return Next entry.
-                     */
-                    template<typename T>
-                    T GetNext(IgniteError& err)
-                    {
-                        if (IsValid()) {
-                            ++processed;
-                            return reader.ReadTopObject<T>();
-                        }
-                        else
-                        {
-                            err = IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                                "Instance is not usable (did you check for error?).");
-
-                            return T();
-                        }
-                    }
-
-                    /**
-                     * Get next entry assuming it's an array of 8-byte signed
-                     * integers. Maps to "byte[]" type in Java.
-                     *
-                     * @param dst Array to store data to.
-                     * @param len Expected length of array.
-                     * @return Actual amount of elements read. If "len" argument is less than actual
-                     *     array size or resulting array is set to null, nothing will be written
-                     *     to resulting array and returned value will contain required array length.
-                     *     -1 will be returned in case array in stream was null.
-                     */
-                    int32_t GetNextInt8Array(int8_t* dst, int32_t len)
-                    {
-                        if (IsValid()) {
-
-                            int32_t actualLen = reader.ReadInt8Array(dst, len);
-
-                            if (actualLen == 0 || (dst && len >= actualLen))
-                                ++processed;
-
-                            return actualLen;
-                        }
-                        else
-                        {
-                            throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                                "Instance is not usable (did you check for error?).");
-                        }
-                    }
-
-                    /**
-                     * Check if the instance is valid.
-                     *
-                     * Invalid instance can be returned if some of the previous
-                     * operations have resulted in a failure. For example invalid
-                     * instance can be returned by not-throwing version of method
-                     * in case of error. Invalid instances also often can be
-                     * created using default constructor.
-                     *
-                     * @return True if the instance is valid and can be used.
-                     */
-                    bool IsValid()
-                    {
-                        return mem.Get() != 0;
-                    }
-
-                private:
-                    /** Row memory. */
-                    SP_InteropMemory mem;
-
-                    /** Row data stream. */
-                    interop::InteropInputStream stream;
-
-                    /** Row data reader. */
-                    binary::BinaryReaderImpl reader;
-
-                    /** Number of elements in a row. */
-                    int32_t columnNum;
-
-                    /** Number of elements that have been read by now. */
-                    int32_t processed;
-
-                    IGNITE_NO_COPY_ASSIGNMENT(QueryFieldsRowImpl);
-                };
-            }
-        }
-    }
-}
-
-#endif //_IGNITE_IMPL_CACHE_QUERY_CACHE_QUERY_FIELDS_ROW_IMPL
diff --git a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/writable_key.h b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/writable_key.h
index bf16fd1..9b72796 100644
--- a/modules/platforms/cpp/thin-client/include/ignite/impl/thin/writable_key.h
+++ b/modules/platforms/cpp/thin-client/include/ignite/impl/thin/writable_key.h
@@ -508,6 +508,7 @@
                  *
                  * @return Hash code of the value.
                  */
+                IGNORE_SIGNED_OVERFLOW
                 virtual int32_t GetHashCode() const
                 {
                     int32_t hash = 0;
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_client.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_client.h
index e53ad98..544010e 100644
--- a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_client.h
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_client.h
@@ -27,12 +27,15 @@
 
 #include <ignite/thin/cache/query/query_fields_cursor.h>
 #include <ignite/thin/cache/query/query_sql_fields.h>
+#include <ignite/thin/cache/query/continuous/continuous_query_client.h>
+#include <ignite/thin/cache/query/continuous/continuous_query_handle.h>
 
 #include <ignite/impl/thin/writable.h>
 #include <ignite/impl/thin/writable_key.h>
 
 #include <ignite/impl/thin/readable.h>
 #include <ignite/impl/thin/cache/cache_client_proxy.h>
+#include <ignite/impl/thin/cache/continuous/continuous_query_client_holder.h>
 
 namespace ignite
 {
@@ -72,7 +75,7 @@
                  *
                  * @param impl Implementation.
                  */
-                CacheClient(common::concurrent::SharedPointer<void> impl) :
+                explicit CacheClient(const common::concurrent::SharedPointer<void>& impl) :
                     proxy(impl)
                 {
                     // No-op.
@@ -597,6 +600,22 @@
                 }
 
                 /**
+                 * Starts the continuous query execution
+                 *
+                 * @param continuousQuery Continuous query.
+                 * @return Query handle. Once all instances are destroyed query execution stopped.
+                 */
+                query::continuous::ContinuousQueryHandleClient QueryContinuous(
+                    query::continuous::ContinuousQueryClient<K, V> continuousQuery)
+                {
+                    using namespace impl::thin::cache::query::continuous;
+
+                    SP_ContinuousQueryClientHolderBase holder(new ContinuousQueryClientHolder<K, V>(continuousQuery));
+
+                    return proxy.QueryContinuous(holder);
+                }
+
+                /**
                  * Refresh affinity mapping.
                  *
                  * @deprecated Does nothing since Apache Ignite 2.8. Affinity mapping is refreshed automatically now.
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_entry.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_entry.h
new file mode 100644
index 0000000..ef52814
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/cache_entry.h
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+
+/**
+ * @file
+ * Declares ignite::thin::cache::CacheEntry class.
+ */
+
+#ifndef _IGNITE_THIN_CACHE_CACHE_ENTRY
+#define _IGNITE_THIN_CACHE_CACHE_ENTRY
+
+#include <utility>
+#include <ignite/common/common.h>
+
+namespace ignite
+{
+    namespace thin
+    {
+        namespace cache
+        {
+            /**
+             * %Cache entry class template.
+             *
+             * Both key and value types should be default-constructable,
+             * copy-constructable and assignable.
+             */
+            template<typename K, typename V>
+            class CacheEntry
+            {
+            public:
+                /**
+                 * Default constructor.
+                 *
+                 * Creates instance with both key and value default-constructed.
+                 */
+                CacheEntry() :
+                    key(),
+                    val(),
+                    hasValue(false)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Constructor.
+                 *
+                 * @param key Key.
+                 * @param val Value.
+                 */
+                CacheEntry(const K& key, const V& val) :
+                    key(key),
+                    val(val),
+                    hasValue(true)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Copy constructor.
+                 *
+                 * @param other Other instance.
+                 */
+                CacheEntry(const CacheEntry& other) :
+                    key(other.key),
+                    val(other.val),
+                    hasValue(other.hasValue)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Constructor.
+                 *
+                 * @param p Pair.
+                 */
+                CacheEntry(const std::pair<K, V>& p) :
+                    key(p.first),
+                    val(p.second),
+                    hasValue(true)
+                {
+                    // No-op.
+                }
+
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~CacheEntry()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Assignment operator.
+                 *
+                 * @param other Other instance.
+                 */
+                CacheEntry& operator=(const CacheEntry& other)
+                {
+                    if (this != &other)
+                    {
+                        key = other.key;
+                        val = other.val;
+                        hasValue = other.hasValue;
+                    }
+
+                    return *this;
+                }
+
+                /**
+                 * Get key.
+                 *
+                 * @return Key.
+                 */
+                const K& GetKey() const
+                {
+                    return key;
+                }
+
+                /**
+                 * Get value.
+                 *
+                 * @return Value.
+                 */
+                const V& GetValue() const
+                {
+                    return val;
+                }
+
+                /**
+                 * Check if the value exists.
+                 *
+                 * @return True, if the value exists.
+                 */
+                bool HasValue() const
+                {
+                    return hasValue;
+                }
+
+            protected:
+                /** Key. */
+                K key;
+
+                /** Value. */
+                V val;
+
+                /** Indicates whether value exists */
+                bool hasValue;
+            };
+        }
+    }
+}
+
+#endif //_IGNITE_THIN_CACHE_CACHE_ENTRY
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event.h
new file mode 100644
index 0000000..57f69b2
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event.h
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+/**
+ * @file
+ * Declares ignite::thin::cache::event::CacheEntryEvent class.
+ */
+
+#ifndef _IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT
+#define _IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT
+
+#include <ignite/binary/binary_raw_reader.h>
+#include <ignite/thin/cache/cache_entry.h>
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        template<typename K, typename V>
+                        class ContinuousQueryClientHolder;
+                    }
+                }
+            }
+        }
+    }
+    namespace thin
+    {
+        namespace cache
+        {
+            /**
+             * Cache event type.
+             */
+            struct CacheEntryEventType
+            {
+                enum Type
+                {
+                    /** An event type indicating that the cache entry was created. */
+                    CREATED = 0,
+
+                    /** An event type indicating that the cache entry was updated. i.e. a previous */
+                    UPDATED = 1,
+
+                    /** An event type indicating that the cache entry was removed. */
+                    REMOVED = 2,
+
+                    /** An event type indicating that the cache entry was removed by expiration policy. */
+                    EXPIRED = 3
+                };
+
+                static Type FromInt8(int8_t val)
+                {
+                    switch (val)
+                    {
+                        case CREATED:
+                        case UPDATED:
+                        case REMOVED:
+                        case EXPIRED:
+                            return static_cast<Type>(val);
+
+                        default:
+                        {
+                            IGNITE_ERROR_FORMATTED_1(IgniteError::IGNITE_ERR_BINARY,
+                                 "Unsupported CacheEntryEventType", "val", val);
+                        }
+                    }
+                }
+            };
+
+            /**
+             * Cache entry event class template.
+             *
+             * Both key and value types should be default-constructable,
+             * copy-constructable and assignable.
+             */
+            template<typename K, typename V>
+            class CacheEntryEvent : public CacheEntry<K, V>
+            {
+                friend class ignite::impl::thin::cache::query::continuous::ContinuousQueryClientHolder<K, V>;
+
+            public:
+                /**
+                 * Default constructor.
+                 *
+                 * Creates instance with all fields default-constructed.
+                 */
+                CacheEntryEvent() :
+                    CacheEntry<K, V>(),
+                    oldVal(),
+                    hasOldValue(false),
+                    eventType(CacheEntryEventType::CREATED)
+                {
+                    // No-op.
+                }
+    
+                /**
+                 * Copy constructor.
+                 *
+                 * @param other Other instance.
+                 */
+                CacheEntryEvent(const CacheEntryEvent<K, V>& other) :
+                    CacheEntry<K, V>(other),
+                    oldVal(other.oldVal),
+                    hasOldValue(other.hasOldValue),
+                    eventType(other.eventType)
+                {
+                    // No-op.
+                }
+    
+                /**
+                 * Destructor.
+                 */
+                virtual ~CacheEntryEvent()
+                {
+                    // No-op.
+                }
+    
+                /**
+                 * Assignment operator.
+                 *
+                 * @param other Other instance.
+                 * @return *this.
+                 */
+                CacheEntryEvent& operator=(const CacheEntryEvent<K, V>& other)
+                {
+                    if (this != &other)
+                    {
+                        CacheEntry<K, V>::operator=(other);
+    
+                        oldVal = other.oldVal;
+                        hasOldValue = other.hasOldValue;
+                        eventType = other.eventType;
+                    }
+    
+                    return *this;
+                }
+
+                /**
+                 * Get event type.
+                 *
+                 * @see CacheEntryEventType for details on events you can receive.
+                 *
+                 * @return Event type.
+                 */
+                CacheEntryEventType::Type GetEventType() const
+                {
+                    return eventType;
+                };
+    
+                /**
+                 * Get old value.
+                 *
+                 * @return Old value.
+                 */
+                const V& GetOldValue() const
+                {
+                    return oldVal;
+                }
+    
+                /**
+                 * Check if the old value exists.
+                 *
+                 * @return True, if the old value exists.
+                 */
+                bool HasOldValue() const
+                {
+                    return hasOldValue;
+                }
+    
+            private:
+                /**
+                 * Reads cache event using provided raw reader.
+                 *
+                 * @param reader Reader to use.
+                 */
+                void Read(binary::BinaryRawReader& reader)
+                {
+                    this->key = reader.ReadObject<K>();
+
+                    this->hasOldValue = reader.TryReadObject(this->oldVal);
+                    this->hasValue = reader.TryReadObject(this->val);
+
+                    int8_t eventTypeByte = reader.ReadInt8();
+                    this->eventType = CacheEntryEventType::FromInt8(eventTypeByte);
+                }
+
+                /** Old value. */
+                V oldVal;
+    
+                /** Indicates whether old value exists */
+                bool hasOldValue;
+
+                /** Event type. */
+                CacheEntryEventType::Type eventType;
+            };
+        }
+    }
+}
+
+#endif //_IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event_listener.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event_listener.h
new file mode 100644
index 0000000..e2829d9
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/event/cache_entry_event_listener.h
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+/**
+ * @file
+ * Declares ignite::thin::cache::event::CacheEntryEventListener class.
+ */
+
+#ifndef _IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT_LISTENER
+#define _IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT_LISTENER
+
+#include <stdint.h>
+
+#include <ignite/thin/cache/event/cache_entry_event.h>
+
+namespace ignite
+{
+    namespace thin
+    {
+        namespace cache
+        {
+            namespace event
+            {
+                /**
+                 * Cache entry event listener.
+                 */
+                template<typename K, typename V>
+                class CacheEntryEventListener
+                {
+                public:
+                    /**
+                     * Default constructor.
+                     */
+                    CacheEntryEventListener()
+                    {
+                        // No-op.
+                    }
+
+                    /**
+                     * Destructor.
+                     */
+                    virtual ~CacheEntryEventListener()
+                    {
+                        // No-op.
+                    }
+
+                    /**
+                     * Event callback.
+                     *
+                     * @param evts Events.
+                     * @param num Events number.
+                     */
+                    virtual void OnEvent(const CacheEntryEvent<K, V>* evts, uint32_t num) = 0;
+
+                    /**
+                     * Disconnected callback.
+                     *
+                     * Called if channel was disconnected. This also means that continuous query was closed and no more
+                     * events will be provided for this listener.
+                     */
+                    virtual void OnDisconnected() = 0;
+                };
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_THIN_CACHE_EVENT_CACHE_ENTRY_EVENT_LISTENER
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_client.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_client.h
new file mode 100644
index 0000000..10e66ca
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_client.h
@@ -0,0 +1,232 @@
+/*
+ * 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.
+ */
+
+/**
+ * @file
+ * Declares ignite::thin::cache::query::continuous::ContinuousQueryClient class.
+ */
+
+#ifndef _IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
+#define _IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
+
+#include <ignite/reference.h>
+
+#include <ignite/thin/cache/event/cache_entry_event_listener.h>
+
+namespace ignite
+{
+    namespace thin
+    {
+        namespace cache
+        {
+            namespace query
+            {
+                namespace continuous
+                {
+                    /**
+                     * Continuous query client.
+                     *
+                     * Continuous query client allow to register a listener for cache update events. On any update to
+                     * the related cache an event is sent to the client that has executed the query and listener is
+                     * notified on that client.
+                     *
+                     * Continuous query can either be executed on the whole topology or only on local node.
+                     *
+                     * To execute the query over the cache use method
+                     * ignite::thin::cache::CacheClient::QueryContinuous().
+                     */
+                    template<typename K, typename V>
+                    class ContinuousQueryClient
+                    {
+                    public:
+                        /**
+                         * Default value for the buffer size.
+                         */
+                        enum { DEFAULT_BUFFER_SIZE = 1 };
+
+                        /**
+                         * Default value for the time interval.
+                         */
+                        enum { DEFAULT_TIME_INTERVAL = 0 };
+
+                        /**
+                         * Destructor.
+                         */
+                        ~ContinuousQueryClient()
+                        {
+                            // No-op.
+                        }
+
+                        /**
+                         * Constructor.
+                         *
+                         * @param lsnr Event listener. Invoked on the node where continuous query execution has been
+                         * started.
+                         */
+                        explicit ContinuousQueryClient(Reference<event::CacheEntryEventListener<K, V> > lsnr) :
+                            bufferSize(DEFAULT_BUFFER_SIZE),
+                            timeInterval(DEFAULT_TIME_INTERVAL),
+                            includeExpired(false),
+                            listener(lsnr)
+                        {
+                            // No-op.
+                        }
+
+                        /**
+                         * Set buffer size.
+                         *
+                         * When a cache update happens, entry is first put into a buffer. Entries from buffer will be
+                         * sent to the master node only if the buffer is full or time provided via SetTimeInterval is
+                         * exceeded.
+                         *
+                         * @param val Buffer size.
+                         */
+                        void SetBufferSize(int32_t val)
+                        {
+                            bufferSize = val;
+                        }
+
+                        /**
+                         * Get buffer size.
+                         *
+                         * When a cache update happens, entry is first put into a buffer. Entries from buffer will be
+                         * sent to the master node only if the buffer is full or time provided via SetTimeInterval is
+                         * exceeded.
+                         *
+                         * @return Buffer size.
+                         */
+                        int32_t GetBufferSize() const
+                        {
+                            return bufferSize;
+                        }
+
+                        /**
+                         * Set time interval.
+                         *
+                         * When a cache update happens, entry is first put into a buffer. Entries from buffer are sent
+                         * to the master node only if the buffer is full (its size can be changed via SetBufferSize) or
+                         * time provided via this method is exceeded.
+                         *
+                         * Default value is DEFAULT_TIME_INTERVAL, i.e. 0, which means that time check is disabled and
+                         * entries will be sent only when buffer is full.
+                         *
+                         * @param val Time interval in miliseconds.
+                         */
+                        void SetTimeInterval(int64_t val)
+                        {
+                            timeInterval = val;
+                        }
+
+                        /**
+                         * Get time interval.
+                         *
+                         * When a cache update happens, entry is first put into a buffer. Entries from buffer are sent
+                         * to the master node only if the buffer is full (its size can be changed via SetBufferSize) or
+                         * time provided via this method is exceeded.
+                         *
+                         * Default value is DEFAULT_TIME_INTERVAL, i.e. 0, which means that time check is disabled and
+                         * entries will be sent only when buffer is full.
+                         *
+                         * @return Time interval.
+                         */
+                        int64_t GetTimeInterval() const
+                        {
+                            return timeInterval;
+                        }
+
+                        /**
+                         * Sets a value indicating whether to notify about Expired events.
+                         *
+                         * If @c true, then the listener will get notifications about expired cache entries. Otherwise,
+                         * only Created, Updated, and Removed events will be passed to the listener.
+                         *
+                         * Defaults to @c false.
+                         *
+                         * @param val Flag value.
+                         */
+                        void SetIncludeExpired(bool val)
+                        {
+                            includeExpired = val;
+                        }
+
+                        /**
+                         * Gets a value indicating whether to notify about Expired events.
+                         *
+                         * If @c true, then the listener will get notifications about expired cache entries. Otherwise,
+                         * only Created, Updated, and Removed events will be passed to the listener.
+                         *
+                         * Defaults to @c false.
+                         *
+                         * @return Flag value.
+                         */
+                        bool GetIncludeExpired() const
+                        {
+                            return includeExpired;
+                        }
+
+                        /**
+                         * Set cache entry event listener.
+                         *
+                         * @param lsnr Cache entry event listener. Invoked on the
+                         *     node where continuous query execution has been
+                         *     started.
+                         */
+                        void SetListener(Reference<event::CacheEntryEventListener<K, V> > lsnr)
+                        {
+                            listener = lsnr;
+                        }
+
+                        /**
+                         * Get cache entry event listener.
+                         *
+                         * @return Cache entry event listener.
+                         */
+                        const event::CacheEntryEventListener<K, V>& GetListener() const
+                        {
+                            return *listener.Get();
+                        }
+
+                        /**
+                         * Get cache entry event listener.
+                         *
+                         * @return Cache entry event listener.
+                         */
+                        event::CacheEntryEventListener<K, V>& GetListener()
+                        {
+                            return *listener.Get();
+                        }
+
+                    private:
+                        /** Buffer size. */
+                        int32_t bufferSize;
+
+                        /** Time interval. */
+                        int64_t timeInterval;
+
+                        /** Include expired. */
+                        bool includeExpired;
+
+                        /** Listener. */
+                        Reference<event::CacheEntryEventListener<K, V> > listener;
+                    };
+                }
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_CLIENT
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_handle.h b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_handle.h
new file mode 100644
index 0000000..348132e
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/cache/query/continuous/continuous_query_handle.h
@@ -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.
+ */
+
+/**
+ * @file
+ * Declares ignite::thin::cache::query::continuous::ContinuousQueryHandleClient class.
+ */
+
+#ifndef _IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
+#define _IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
+
+#include <ignite/common/concurrent.h>
+
+namespace ignite
+{
+    namespace thin
+    {
+        namespace cache
+        {
+            namespace query
+            {
+                namespace continuous
+                {
+                    /**
+                     * Continuous query handle client.
+                     *
+                     * Once destructed, continuous query is stopped and event listener stops getting event.
+                     */
+                    class ContinuousQueryHandleClient
+                    {
+                    public:
+                        /**
+                         * Default constructor.
+                         */
+                        ContinuousQueryHandleClient() :
+                            impl()
+                        {
+                            // No-op.
+                        }
+
+                        /**
+                         * Constructor.
+                         *
+                         * Internal method. Should not be used by user.
+                         *
+                         * @param impl Implementation.
+                         */
+                        ContinuousQueryHandleClient(const common::concurrent::SharedPointer<void>& impl) :
+                            impl(impl)
+                        {
+                            // No-op.
+                        }
+
+                    private:
+                        /** Implementation delegate. */
+                        common::concurrent::SharedPointer<void> impl;
+                    };
+                }
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/include/ignite/thin/ignite_client_configuration.h b/modules/platforms/cpp/thin-client/include/ignite/thin/ignite_client_configuration.h
index acb8a53..68750a2 100644
--- a/modules/platforms/cpp/thin-client/include/ignite/thin/ignite_client_configuration.h
+++ b/modules/platforms/cpp/thin-client/include/ignite/thin/ignite_client_configuration.h
@@ -39,6 +39,9 @@
         class IgniteClientConfiguration
         {
         public:
+            /** Connection operation timeout in milliseconds. */
+            enum { DEFAULT_CONNECTION_TIMEOUT = 20000 };
+
             /**
              * Default constructor.
              *
@@ -47,7 +50,8 @@
             IgniteClientConfiguration() :
                 sslMode(SslMode::DISABLE),
                 partitionAwareness(true),
-                connectionsLimit(0)
+                connectionsLimit(0),
+                connectionTimeout(DEFAULT_CONNECTION_TIMEOUT)
             {
                 // No-op.
             }
@@ -70,7 +74,7 @@
              *
              * For example: "localhost,example.com:12345,127.0.0.1:10800..10900,192.168.3.80:5893".
              *
-             *  @param endPoints Addressess of the remote servers to connect.
+             *  @param endPoints Addresses of the remote servers to connect.
              */
             void SetEndPoints(const std::string& endPoints)
             {
@@ -262,6 +266,34 @@
                 connectionsLimit = limit;
             }
 
+            /**
+             * Get connection timeout.
+             *
+             * Used as a timeout for any operation performed over TCP sockets.
+             *
+             * Zero value means that there is no timeout.
+             *
+             * The default value is @c DEFAULT_CONNECTION_TIMEOUT.
+             *
+             * @return Connection timeout in milliseconds.
+             */
+            int32_t GetConnectionTimeout() const
+            {
+                return connectionTimeout;
+            }
+
+            /**
+             * Set connection timeout.
+             *
+             * @see GetConnectionTimeout for details.
+             *
+             * @param timeout Connection timeout in milliseconds to set.
+             */
+            void SetConnectionTimeout(int32_t timeout)
+            {
+                connectionTimeout = timeout;
+            }
+
         private:
             /** Connection end points */
             std::string endPoints;
@@ -289,6 +321,9 @@
 
             /** Active connections limit. */
             uint32_t connectionsLimit;
+
+            /** Connection timeout in milliseconds. */
+            int32_t connectionTimeout;
         };
     }
 }
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.cpp b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.cpp
index 6be7064..100645a 100644
--- a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.cpp
@@ -20,6 +20,7 @@
 #include "impl/response_status.h"
 #include "impl/message.h"
 #include "impl/cache/cache_client_impl.h"
+#include "impl/cache/query/continuous/continuous_query_notification_handler.h"
 #include "impl/transactions/transactions_impl.h"
 
 using namespace ignite::impl::thin::transactions;
@@ -55,7 +56,7 @@
                 }
 
                 template<typename ReqT, typename RspT>
-                void CacheClientImpl::SyncCacheKeyMessage(const WritableKey& key, const ReqT& req, RspT& rsp)
+                void CacheClientImpl::SyncCacheKeyMessage(const WritableKey& key, ReqT& req, RspT& rsp)
                 {
                     DataRouter& router0 = *router.Get();
 
@@ -91,7 +92,7 @@
                 }
 
                 template<typename ReqT, typename RspT>
-                SP_DataChannel CacheClientImpl::SyncMessage(const ReqT& req, RspT& rsp)
+                SP_DataChannel CacheClientImpl::SyncMessage(ReqT& req, RspT& rsp)
                 {
                     SP_DataChannel channel = router.Get()->SyncMessage(req, rsp);
 
@@ -102,7 +103,7 @@
                 }
 
                 template<typename ReqT, typename RspT>
-                SP_DataChannel CacheClientImpl::SyncMessageSql(const ReqT& req, RspT& rsp)
+                SP_DataChannel CacheClientImpl::SyncMessageSql(ReqT& req, RspT& rsp)
                 {
                     SP_DataChannel channel;
                     try {
@@ -159,7 +160,7 @@
 
                 void CacheClientImpl::Put(const WritableKey& key, const Writable& value)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_PUT> req(id, binary, key, value);
+                    Cache2ValueRequest<MessageType::CACHE_PUT> req(id, binary, key, value);
                     Response rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -167,7 +168,7 @@
 
                 void CacheClientImpl::Get(const WritableKey& key, Readable& value)
                 {
-                    CacheValueRequest<RequestType::CACHE_GET> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_GET> req(id, binary, key);
                     CacheValueResponse rsp(value);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -175,7 +176,7 @@
 
                 void CacheClientImpl::PutAll(const Writable & pairs)
                 {
-                    CacheValueRequest<RequestType::CACHE_PUT_ALL> req(id, binary, pairs);
+                    CacheValueRequest<MessageType::CACHE_PUT_ALL> req(id, binary, pairs);
                     Response rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -183,7 +184,7 @@
 
                 void CacheClientImpl::GetAll(const Writable& keys, Readable& pairs)
                 {
-                    CacheValueRequest<RequestType::CACHE_GET_ALL> req(id, binary, keys);
+                    CacheValueRequest<MessageType::CACHE_GET_ALL> req(id, binary, keys);
                     CacheValueResponse rsp(pairs);
 
                     TransactionalSyncMessage(req, rsp);
@@ -191,7 +192,7 @@
 
                 bool CacheClientImpl::Replace(const WritableKey& key, const Writable& value)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_REPLACE> req(id, binary, key, value);
+                    Cache2ValueRequest<MessageType::CACHE_REPLACE> req(id, binary, key, value);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -201,7 +202,7 @@
 
                 bool CacheClientImpl::ContainsKey(const WritableKey& key)
                 {
-                    CacheValueRequest<RequestType::CACHE_CONTAINS_KEY> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_CONTAINS_KEY> req(id, binary, key);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -211,7 +212,7 @@
 
                 bool CacheClientImpl::ContainsKeys(const Writable& keys)
                 {
-                    CacheValueRequest<RequestType::CACHE_CONTAINS_KEYS> req(id, binary, keys);
+                    CacheValueRequest<MessageType::CACHE_CONTAINS_KEYS> req(id, binary, keys);
                     BoolResponse rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -231,7 +232,7 @@
 
                 bool CacheClientImpl::Remove(const WritableKey& key)
                 {
-                    CacheValueRequest<RequestType::CACHE_REMOVE_KEY> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_REMOVE_KEY> req(id, binary, key);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -241,7 +242,7 @@
 
                 bool CacheClientImpl::Remove(const WritableKey& key, const Writable& val)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_REMOVE_IF_EQUALS> req(id, binary, key, val);
+                    Cache2ValueRequest<MessageType::CACHE_REMOVE_IF_EQUALS> req(id, binary, key, val);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -251,7 +252,7 @@
 
                 void CacheClientImpl::RemoveAll(const Writable& keys)
                 {
-                    CacheValueRequest<RequestType::CACHE_REMOVE_KEYS> req(id, binary, keys);
+                    CacheValueRequest<MessageType::CACHE_REMOVE_KEYS> req(id, binary, keys);
                     Response rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -259,7 +260,7 @@
 
                 void CacheClientImpl::RemoveAll()
                 {
-                    CacheRequest<RequestType::CACHE_REMOVE_ALL> req(id, binary);
+                    CacheRequest<MessageType::CACHE_REMOVE_ALL> req(id, binary);
                     Response rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -267,7 +268,7 @@
 
                 void CacheClientImpl::Clear(const WritableKey& key)
                 {
-                    CacheValueRequest<RequestType::CACHE_CLEAR_KEY> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_CLEAR_KEY> req(id, binary, key);
                     Response rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -275,7 +276,7 @@
 
                 void CacheClientImpl::Clear()
                 {
-                    CacheRequest<RequestType::CACHE_CLEAR> req(id, binary);
+                    CacheRequest<MessageType::CACHE_CLEAR> req(id, binary);
                     Response rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -283,7 +284,7 @@
 
                 void CacheClientImpl::ClearAll(const Writable& keys)
                 {
-                    CacheValueRequest<RequestType::CACHE_CLEAR_KEYS> req(id, binary, keys);
+                    CacheValueRequest<MessageType::CACHE_CLEAR_KEYS> req(id, binary, keys);
                     Response rsp;
 
                     TransactionalSyncMessage(req, rsp);
@@ -291,7 +292,7 @@
 
                 void CacheClientImpl::LocalPeek(const WritableKey& key, Readable& value)
                 {
-                    CacheValueRequest<RequestType::CACHE_LOCAL_PEEK> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_LOCAL_PEEK> req(id, binary, key);
                     CacheValueResponse rsp(value);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -299,7 +300,7 @@
 
                 bool CacheClientImpl::Replace(const WritableKey& key, const Writable& oldVal, const Writable& newVal)
                 {
-                    Cache3ValueRequest<RequestType::CACHE_REPLACE_IF_EQUALS> req(id, binary, key, oldVal, newVal);
+                    Cache3ValueRequest<MessageType::CACHE_REPLACE_IF_EQUALS> req(id, binary, key, oldVal, newVal);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -309,7 +310,7 @@
 
                 void CacheClientImpl::GetAndPut(const WritableKey& key, const Writable& valIn, Readable& valOut)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_GET_AND_PUT> req(id, binary, key, valIn);
+                    Cache2ValueRequest<MessageType::CACHE_GET_AND_PUT> req(id, binary, key, valIn);
                     CacheValueResponse rsp(valOut);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -317,7 +318,7 @@
 
                 void CacheClientImpl::GetAndRemove(const WritableKey& key, Readable& valOut)
                 {
-                    CacheValueRequest<RequestType::CACHE_GET_AND_REMOVE> req(id, binary, key);
+                    CacheValueRequest<MessageType::CACHE_GET_AND_REMOVE> req(id, binary, key);
                     CacheValueResponse rsp(valOut);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -325,7 +326,7 @@
 
                 void CacheClientImpl::GetAndReplace(const WritableKey& key, const Writable& valIn, Readable& valOut)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_GET_AND_REPLACE> req(id, binary, key, valIn);
+                    Cache2ValueRequest<MessageType::CACHE_GET_AND_REPLACE> req(id, binary, key, valIn);
                     CacheValueResponse rsp(valOut);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -333,7 +334,7 @@
 
                 bool CacheClientImpl::PutIfAbsent(const WritableKey& key, const Writable& val)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_PUT_IF_ABSENT> req(id, binary, key, val);
+                    Cache2ValueRequest<MessageType::CACHE_PUT_IF_ABSENT> req(id, binary, key, val);
                     BoolResponse rsp;
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -343,7 +344,7 @@
 
                 void CacheClientImpl::GetAndPutIfAbsent(const WritableKey& key, const Writable& valIn, Readable& valOut)
                 {
-                    Cache2ValueRequest<RequestType::CACHE_GET_AND_PUT_IF_ABSENT> req(id, binary, key, valIn);
+                    Cache2ValueRequest<MessageType::CACHE_GET_AND_PUT_IF_ABSENT> req(id, binary, key, valIn);
                     CacheValueResponse rsp(valOut);
 
                     TransactionalSyncCacheKeyMessage(key, req, rsp);
@@ -367,6 +368,25 @@
 
                     return cursorImpl;
                 }
+
+                query::continuous::SP_ContinuousQueryHandleClientImpl CacheClientImpl::QueryContinuous(
+                        const query::continuous::SP_ContinuousQueryClientHolderBase& continuousQuery)
+                {
+                    const query::continuous::ContinuousQueryClientHolderBase& cq = *continuousQuery.Get();
+
+                    ContinuousQueryRequest req(id, cq.GetBufferSize(), cq.GetTimeInterval(), cq.GetIncludeExpired());
+                    ContinuousQueryResponse rsp;
+
+                    SP_DataChannel channel = SyncMessage(req, rsp);
+
+                    query::continuous::SP_ContinuousQueryNotificationHandler handler(
+                            new query::continuous::ContinuousQueryNotificationHandler(*channel.Get(), continuousQuery));
+
+                    channel.Get()->RegisterNotificationHandler(rsp.GetQueryId(), handler);
+
+                    return query::continuous::SP_ContinuousQueryHandleClientImpl(
+                        new query::continuous::ContinuousQueryHandleClientImpl(channel, rsp.GetQueryId()));
+                }
             }
         }
     }
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.h b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.h
index a7ab0be..4c82592 100644
--- a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.h
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_impl.h
@@ -23,9 +23,12 @@
 
 #include <ignite/thin/cache/query/query_sql_fields.h>
 
+#include <ignite/impl/thin/cache/continuous/continuous_query_client_holder.h>
+
 #include "impl/data_router.h"
 #include "impl/transactions/transactions_impl.h"
 #include "impl/cache/query/query_fields_cursor_impl.h"
+#include "impl/cache/query/continuous/continuous_query_handle_impl.h"
 
 namespace ignite
 {
@@ -298,6 +301,15 @@
                      */
                     query::SP_QueryFieldsCursorImpl Query(const ignite::thin::cache::query::SqlFieldsQuery &qry);
 
+                    /**
+                     * Starts the continuous query execution
+                     *
+                     * @param continuousQuery Continuous query.
+                     * @return Query handle. Once all instances are destroyed query execution stopped.
+                     */
+                    query::continuous::SP_ContinuousQueryHandleClientImpl QueryContinuous(
+                            const query::continuous::SP_ContinuousQueryClientHolderBase& continuousQuery);
+
                 private:
                     /**
                      * Synchronously send request message and receive response.
@@ -308,7 +320,7 @@
                      * @throw IgniteError on error.
                      */
                     template<typename ReqT, typename RspT>
-                    void SyncCacheKeyMessage(const WritableKey& key, const ReqT& req, RspT& rsp);
+                    void SyncCacheKeyMessage(const WritableKey& key, ReqT& req, RspT& rsp);
 
                     /**
                      * Synchronously send message and receive response.
@@ -319,7 +331,7 @@
                      * @throw IgniteError on error.
                      */
                     template<typename ReqT, typename RspT>
-                    SP_DataChannel SyncMessage(const ReqT& req, RspT& rsp);
+                    SP_DataChannel SyncMessage(ReqT& req, RspT& rsp);
 
                     /**
                      * Synchronously send message and receive response.
@@ -331,7 +343,7 @@
                      * @throw IgniteError on error.
                      */
                     template<typename ReqT, typename RspT>
-                    SP_DataChannel SyncMessageSql(const ReqT& req, RspT& rsp);
+                    SP_DataChannel SyncMessageSql(ReqT& req, RspT& rsp);
 
                     /**
                      * Synchronously send request message and receive response taking in account that it can be
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_proxy.cpp b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_proxy.cpp
index a5795ee..eab6ba0 100644
--- a/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_proxy.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/cache_client_proxy.cpp
@@ -15,12 +15,18 @@
  * limitations under the License.
  */
 
+#include <ignite/thin/cache/query/continuous/continuous_query_handle.h>
+
 #include <ignite/impl/thin/cache/cache_client_proxy.h>
 #include <impl/cache/cache_client_impl.h>
 
 using namespace ignite::impl::thin;
 using namespace cache;
 
+using ignite::thin::cache::query::SqlFieldsQuery;
+using ignite::thin::cache::query::QueryFieldsCursor;
+using ignite::thin::cache::query::continuous::ContinuousQueryHandleClient;
+
 namespace
 {
     using namespace ignite::common::concurrent;
@@ -150,13 +156,18 @@
                     GetCacheImpl(impl).GetAndPutIfAbsent(key, valIn, valOut);
                 }
 
-                ignite::thin::cache::query::QueryFieldsCursor CacheClientProxy::Query(
-                        const ignite::thin::cache::query::SqlFieldsQuery &qry)
+                QueryFieldsCursor CacheClientProxy::Query(const ignite::thin::cache::query::SqlFieldsQuery &qry)
                 {
                     query::SP_QueryFieldsCursorImpl cursorImpl = GetCacheImpl(impl).Query(qry);
 
                     return ignite::thin::cache::query::QueryFieldsCursor(cursorImpl);
                 }
+
+                ContinuousQueryHandleClient CacheClientProxy::QueryContinuous(
+                    const query::continuous::SP_ContinuousQueryClientHolderBase &continuousQuery)
+                {
+                    return ContinuousQueryHandleClient(GetCacheImpl(impl).QueryContinuous(continuousQuery));
+                }
             }
         }
     }
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_handle_impl.h b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_handle_impl.h
new file mode 100644
index 0000000..2843d30
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_handle_impl.h
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
+#define _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
+
+#include <ignite/common/common.h>
+#include <ignite/common/concurrent.h>
+
+#include "impl/data_channel.h"
+#include "impl/message.h"
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        /**
+                         * Continuous query handle client implementation.
+                         *
+                         * Once destructed, continuous query is stopped and event listener stops getting event.
+                         */
+                        class ContinuousQueryHandleClientImpl
+                        {
+                        public:
+                            /**
+                             * Default constructor.
+                             *
+                             * @param channel Channel.
+                             * @param queryId Query ID.
+                             */
+                            ContinuousQueryHandleClientImpl(SP_DataChannel channel, int64_t queryId) :
+                                channel(channel),
+                                queryId(queryId)
+                            {
+                                // No-op.
+                            }
+
+                            /**
+                             * Destructor.
+                             */
+                            virtual ~ContinuousQueryHandleClientImpl()
+                            {
+                                assert(channel.IsValid());
+
+                                DataChannel& channel0 = *channel.Get();
+
+                                channel0.DeregisterNotificationHandler(queryId);
+                                channel0.CloseResource(queryId);
+                            }
+
+                        private:
+                            /** Data channel. */
+                            SP_DataChannel channel;
+
+                            /** Query ID. */
+                            int64_t queryId;
+                        };
+
+                        /** Shared pointer to ContinuousQueryHandleClientImpl. */
+                        typedef common::concurrent::SharedPointer<ContinuousQueryHandleClientImpl> SP_ContinuousQueryHandleClientImpl;
+                    }
+                }
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_HANDLE_CLIENT
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.cpp b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.cpp
new file mode 100644
index 0000000..96449dd
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#include "impl/data_channel.h"
+#include "impl/message.h"
+
+#include "impl/cache/query/continuous/continuous_query_notification_handler.h"
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        ContinuousQueryNotificationHandler::ContinuousQueryNotificationHandler(DataChannel& channel,
+                            const SP_ContinuousQueryClientHolderBase& continuousQuery) :
+                            continuousQuery(continuousQuery),
+                            channel(channel)
+                        {
+                            // No-op.
+                        }
+
+                        ContinuousQueryNotificationHandler::~ContinuousQueryNotificationHandler()
+                        {
+                            // No-op.
+                        }
+
+                        void ContinuousQueryNotificationHandler::OnNotification(const network::DataBuffer& msg)
+                        {
+                            ClientCacheEntryEventNotification notification(*continuousQuery.Get());
+                            channel.DeserializeMessage(msg, notification);
+                        }
+
+                        void ContinuousQueryNotificationHandler::OnDisconnected()
+                        {
+                            continuousQuery.Get()->OnDisconnected();
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.h b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.h
new file mode 100644
index 0000000..b1bc955
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/query/continuous/continuous_query_notification_handler.h
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_NOTIFICATION_HANDLER
+#define _IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_NOTIFICATION_HANDLER
+
+#include <ignite/common/common.h>
+#include <ignite/common/concurrent.h>
+
+#include <ignite/binary/binary_raw_reader.h>
+
+#include <ignite/impl/interop/interop_input_stream.h>
+#include <ignite/impl/binary/binary_reader_impl.h>
+#include <ignite/impl/thin/cache/continuous/continuous_query_client_holder.h>
+
+#include "impl/notification_handler.h"
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        /**
+                         * Continuous query notification handler.
+                         */
+                        class ContinuousQueryNotificationHandler : public NotificationHandler
+                        {
+                        public:
+                            /**
+                             * Constructor.
+                             *
+                             * @param channel Channel.
+                             * @param continuousQuery Continuous Query.
+                             */
+                            ContinuousQueryNotificationHandler(DataChannel& channel,
+                                const SP_ContinuousQueryClientHolderBase& continuousQuery);
+
+                            /**
+                             * Destructor.
+                             */
+                            virtual ~ContinuousQueryNotificationHandler();
+
+                            /**
+                             * Handle notification.
+                             *
+                             * @param msg Message.
+                             * @return @c true if processing complete.
+                             */
+                            virtual void OnNotification(const network::DataBuffer& msg);
+
+                            /**
+                             * Disconnected callback.
+                             *
+                             * Called if channel was disconnected.
+                             */
+                            virtual void OnDisconnected();
+
+                        private:
+                            /** Query. */
+                            SP_ContinuousQueryClientHolderBase continuousQuery;
+
+                            /** Channel. */
+                            DataChannel& channel;
+                        };
+
+                        /** Shared pointer to ContinuousQueryHandleClientImpl. */
+                        typedef common::concurrent::SharedPointer<ContinuousQueryNotificationHandler> SP_ContinuousQueryNotificationHandler;
+                    }
+                }
+            }
+        }
+    }
+}
+
+#endif //_IGNITE_IMPL_THIN_CACHE_QUERY_CONTINUOUS_CONTINUOUS_QUERY_NOTIFICATION_HANDLER
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/src/impl/cache/query/cursor_page.h b/modules/platforms/cpp/thin-client/src/impl/cache/query/cursor_page.h
index 34be0a7..f23aa09 100644
--- a/modules/platforms/cpp/thin-client/src/impl/cache/query/cursor_page.h
+++ b/modules/platforms/cpp/thin-client/src/impl/cache/query/cursor_page.h
@@ -67,11 +67,12 @@
                             startPos = stream->Position();
 
                             interop::InteropUnpooledMemory* streamMem =
-                                static_cast<interop::InteropUnpooledMemory*>(stream->GetMemory());
+                                const_cast<interop::InteropUnpooledMemory*>(
+                                    static_cast<const interop::InteropUnpooledMemory*>(stream->GetMemory()));
 
                             bool gotOwnership = streamMem->TryGetOwnership(mem);
 
-                            (void) gotOwnership;
+                            IGNITE_UNUSED(gotOwnership);
                             assert(gotOwnership);
                         }
 
diff --git a/modules/platforms/cpp/thin-client/src/impl/channel_state_handler.h b/modules/platforms/cpp/thin-client/src/impl/channel_state_handler.h
new file mode 100644
index 0000000..e8954dc
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/src/impl/channel_state_handler.h
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_IMPL_THIN_CHANNEL_STATE_HANDLER
+#define _IGNITE_IMPL_THIN_CHANNEL_STATE_HANDLER
+
+#include <stdint.h>
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            /** Channel state handler. */
+            class ChannelStateHandler
+            {
+            public:
+                /**
+                 * Destructor.
+                 */
+                virtual ~ChannelStateHandler()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Channel handshake completion callback.
+                 *
+                 * @param id Channel ID.
+                 */
+                virtual void OnHandshakeSuccess(uint64_t id) = 0;
+
+                /**
+                 * Channel handshake error callback.
+                 *
+                 * @param id Channel ID.
+                 * @param err Error.
+                 */
+                virtual void OnHandshakeError(uint64_t id, const IgniteError& err) = 0;
+
+                /**
+                 * Called if notification handling failed.
+                 *
+                 * @param id Channel ID.
+                 * @param err Error.
+                 */
+                virtual void OnNotificationHandlingError(uint64_t id, const IgniteError& err) = 0;
+            };
+        }
+    }
+}
+
+#endif //_IGNITE_IMPL_THIN_CHANNEL_STATE_HANDLER
\ No newline at end of file
diff --git a/modules/platforms/cpp/thin-client/src/impl/compute/compute_client_impl.cpp b/modules/platforms/cpp/thin-client/src/impl/compute/compute_client_impl.cpp
index b42fab3..53de78f 100644
--- a/modules/platforms/cpp/thin-client/src/impl/compute/compute_client_impl.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/compute/compute_client_impl.cpp
@@ -15,11 +15,83 @@
  * limitations under the License.
  */
 
+#include <ignite/common/promise.h>
+
 #include "impl/compute/compute_client_impl.h"
 #include "impl/message.h"
 
 using namespace ignite::common::concurrent;
 
+namespace
+{
+    using namespace ignite;
+    using namespace impl;
+    using namespace impl::thin;
+
+    /**
+     * Handler for java task notification.
+     */
+    class JavaTaskNotificationHandler : public NotificationHandler
+    {
+    public:
+        /**
+         * Constructor.
+         * @param channel Channel.
+         * @param res Result.
+         */
+        JavaTaskNotificationHandler(DataChannel& channel, Readable& res) :
+            channel(channel),
+            res(res)
+        {
+            // No-op.
+        }
+
+        virtual ~JavaTaskNotificationHandler()
+        {
+            // No-op.
+        }
+
+        virtual void OnNotification(const network::DataBuffer& msg)
+        {
+            ComputeTaskFinishedNotification notification(res);
+            channel.DeserializeMessage(msg, notification);
+
+            if (notification.IsFailure())
+            {
+                promise.SetError(IgniteError(IgniteError::IGNITE_ERR_COMPUTE_EXECUTION_REJECTED,
+                    notification.GetError().c_str()));
+            }
+            else
+                promise.SetValue();
+        }
+
+        virtual void OnDisconnected()
+        {
+            promise.SetError(IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, "Connection closed"));
+        }
+
+        /**
+         * Get future result.
+         *
+         * @return Future.
+         */
+        ignite::Future<void> GetFuture() const
+        {
+            return promise.GetFuture();
+        }
+
+    private:
+        /** Channel. */
+        DataChannel& channel;
+
+        /** Result. */
+        Readable& res;
+
+        /** Completion promise. */
+        ignite::common::Promise<void> promise;
+    };
+}
+
 namespace ignite
 {
     namespace impl
@@ -32,14 +104,18 @@
                     Writable& wrArg, Readable& res)
                 {
                     ComputeTaskExecuteRequest req(flags, timeout, taskName, wrArg);
-                    ComputeTaskFinishedNotification notification(res);
+                    ComputeTaskExecuteResponse rsp;
 
-                    router.Get()->SyncMessageWithNotification(req, notification);
+                    SP_DataChannel channel = router.Get()->SyncMessage(req, rsp);
 
-                    if (notification.IsFailure())
-                        throw IgniteError(IgniteError::IGNITE_ERR_COMPUTE_TASK_CANCELLED,
-                            notification.GetErrorMessage().c_str());
+                    common::concurrent::SharedPointer<JavaTaskNotificationHandler> handler(
+                        new JavaTaskNotificationHandler(*channel.Get(), res));
 
+                    channel.Get()->RegisterNotificationHandler(rsp.GetNotificationId(), handler);
+
+                    handler.Get()->GetFuture().GetValue();
+
+                    channel.Get()->DeregisterNotificationHandler(rsp.GetNotificationId());
                 }
             }
         }
diff --git a/modules/platforms/cpp/thin-client/src/impl/data_channel.cpp b/modules/platforms/cpp/thin-client/src/impl/data_channel.cpp
index df219d0..9c7194a 100644
--- a/modules/platforms/cpp/thin-client/src/impl/data_channel.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/data_channel.cpp
@@ -18,10 +18,11 @@
 #include <cstddef>
 
 #include <ignite/common/fixed_size_array.h>
+#include <ignite/common/promise.h>
+
 #include <ignite/network/network.h>
 
 #include "impl/message.h"
-#include "impl/remote_type_updater.h"
 #include "impl/data_channel.h"
 
 namespace ignite
@@ -50,246 +51,205 @@
             const DataChannel::VersionSet DataChannel::supportedVersions(supportedArray,
                 supportedArray + (sizeof(supportedArray) / sizeof(supportedArray[0])));
 
-            DataChannel::DataChannel(const ignite::thin::IgniteClientConfiguration& cfg,
-                binary::BinaryTypeManager& typeMgr) :
-                ioMutex(),
-                node(),
+            DataChannel::DataChannel(
+                uint64_t id,
+                const network::EndPoint& addr,
+                const ignite::network::SP_AsyncClientPool& asyncPool,
+                const ignite::thin::IgniteClientConfiguration& cfg,
+                binary::BinaryTypeManager& typeMgr,
+                ChannelStateHandler& stateHandler,
+                common::ThreadPool& userThreadPool
+            ) :
+                stateHandler(stateHandler),
+                handshakePerformed(false),
+                id(id),
+                asyncPool(asyncPool),
+                node(addr),
                 config(cfg),
                 typeMgr(typeMgr),
                 currentVersion(VERSION_DEFAULT),
                 reqIdCounter(0),
-                socket()
+                responseMutex(),
+                userThreadPool(userThreadPool)
             {
                 // No-op.
             }
 
             DataChannel::~DataChannel()
             {
-                Close();
+                Close(0);
             }
 
-            bool DataChannel::Connect(const std::string& host, uint16_t port, int32_t timeout)
+            void DataChannel::StartHandshake()
             {
-                using ignite::thin::SslMode;
+                DoHandshake(VERSION_DEFAULT);
+            }
 
-                if (socket.get() != 0)
-                    throw IgniteError(IgniteError::IGNITE_ERR_ILLEGAL_STATE, "Already connected.");
+            void DataChannel::Close(const IgniteError* err)
+            {
+                asyncPool.Get()->Close(id, err);
+            }
 
-                if (config.GetEndPoints().empty())
-                    throw IgniteError(IgniteError::IGNITE_ERR_ILLEGAL_ARGUMENT, "No valid address to connect.");
+            void DataChannel::SyncMessage(Request &req, Response &rsp, int32_t timeout)
+            {
+                Future<network::DataBuffer> rspFut = AsyncMessage(req);
 
-                SslMode::Type sslMode = config.GetSslMode();
+                bool success = true;
+                if (timeout)
+                    success = rspFut.WaitFor(timeout);
+                else
+                    rspFut.Wait();
 
-                if (sslMode != SslMode::DISABLE)
+                if (!success)
                 {
-                    socket.reset(network::ssl::MakeSecureSocketClient(
-                        config.GetSslCertFile(), config.GetSslKeyFile(), config.GetSslCaFile()));
+                    common::concurrent::CsLockGuard lock(responseMutex);
+
+                    responseMap.erase(req.GetId());
+
+                    std::string msg = "Can not send message to remote host " +
+                        node.GetEndPoint().ToString() + " within timeout.";
+
+                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
+                }
+
+                DeserializeMessage(rspFut.GetValue(), rsp);
+            }
+
+            int64_t DataChannel::GenerateRequestMessage(Request &req, interop::InteropMemory &mem)
+            {
+                interop::InteropOutputStream outStream(&mem);
+                binary::BinaryWriterImpl writer(&outStream, &typeMgr);
+
+                // Space for RequestSize + OperationCode + RequestID.
+                outStream.Reserve(4 + 2 + 8);
+
+                req.Write(writer, currentVersion);
+
+                int64_t reqId = GenerateRequestId();
+                req.SetId(reqId);
+
+                outStream.WriteInt32(0, outStream.Position() - 4);
+                outStream.WriteInt16(4, req.GetOperationCode());
+                outStream.WriteInt64(6, reqId);
+
+                outStream.Synchronize();
+
+                return reqId;
+            }
+
+            Future<network::DataBuffer> DataChannel::AsyncMessage(Request &req)
+            {
+                // Allocating 64 KB to decrease number of re-allocations.
+                enum { BUFFER_SIZE = 1024 * 64 };
+
+                interop::SP_InteropMemory mem(new interop::InteropUnpooledMemory(BUFFER_SIZE));
+
+                int64_t reqId = GenerateRequestMessage(req, *mem.Get());
+
+                common::concurrent::CsLockGuard lock1(responseMutex);
+                SP_PromiseDataBuffer& sp = responseMap[reqId];
+                if (!sp.IsValid())
+                    sp = SP_PromiseDataBuffer(new common::Promise<network::DataBuffer>());
+
+                Future<network::DataBuffer> future = sp.Get()->GetFuture();
+                lock1.Reset();
+
+                network::DataBuffer buffer(mem);
+                bool success = asyncPool.Get()->Send(id, buffer);
+
+                if (!success)
+                {
+                    common::concurrent::CsLockGuard lock2(responseMutex);
+
+                    responseMap.erase(reqId);
+
+                    std::string msg = "Can not send message to remote host " + node.GetEndPoint().ToString();
+
+                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
+                }
+
+                return future;
+            }
+
+            void DataChannel::ProcessMessage(const network::DataBuffer& msg)
+            {
+                if (!handshakePerformed)
+                {
+                    OnHandshakeResponse(msg);
+                    return;
+                }
+
+                interop::InteropInputStream inStream(msg.GetInputStream());
+
+                inStream.Ignore(4);
+
+                int64_t rspId = inStream.ReadInt64();
+                int16_t flags = inStream.ReadInt16();
+
+                if (flags & Flag::NOTIFICATION)
+                {
+                    common::SP_ThreadPoolTask task;
+                    {
+                        common::concurrent::CsLockGuard lock(handlerMutex);
+
+                        NotificationHandlerHolder& holder = handlerMap[rspId];
+                        task = holder.ProcessNotification(msg, id, stateHandler);
+                    }
+
+                    if (task.IsValid())
+                        userThreadPool.Dispatch(task);
                 }
                 else
-                    socket.reset(network::ssl::MakeTcpSocketClient());
-
-                node = IgniteNode(host, port);
-
-                return TryRestoreConnection(timeout);
-            }
-
-            void DataChannel::Close()
-            {
-                if (socket.get() != 0)
                 {
-                    socket->Close();
+                    common::concurrent::CsLockGuard lock(responseMutex);
 
-                    socket.reset();
-                }
-            }
-
-            void DataChannel::InternalSyncMessage(interop::InteropUnpooledMemory& mem, int32_t timeout)
-            {
-                common::concurrent::CsLockGuard lock(ioMutex);
-
-                InternalSyncMessageUnguarded(mem, timeout);
-            }
-
-            void DataChannel::InternalSyncMessageUnguarded(interop::InteropUnpooledMemory& mem, int32_t timeout)
-            {
-                bool success = Send(mem.Data(), mem.Length(), timeout);
-
-                if (!success)
-                {
-                    success = TryRestoreConnection(timeout);
-
-                    if (!success)
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                                          "Can not send message to remote host: timeout");
-
-                    success = Send(mem.Data(), mem.Length(), timeout);
-
-                    if (!success)
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                                          "Can not send message to remote host: timeout");
-                }
-
-                success = Receive(mem, timeout);
-
-                if (!success)
-                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                                      "Can not receive message response from the remote host: timeout");
-            }
-
-            bool DataChannel::Send(const int8_t* data, size_t len, int32_t timeout)
-            {
-                if (socket.get() == 0)
-                    throw IgniteError(IgniteError::IGNITE_ERR_ILLEGAL_STATE, "Connection is not established");
-
-                OperationResult::T res = SendAll(data, len, timeout);
-
-                if (res == OperationResult::TIMEOUT)
-                    return false;
-
-                if (res == OperationResult::FAIL)
-                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                        "Can not send message due to connection failure");
-
-                return true;
-            }
-
-            DataChannel::OperationResult::T DataChannel::SendAll(const int8_t* data, size_t len, int32_t timeout)
-            {
-                int sent = 0;
-
-                while (sent != static_cast<int64_t>(len))
-                {
-                    int res = socket->Send(data + sent, len - sent, timeout);
-
-                    if (res < 0 || res == network::SocketClient::WaitResult::TIMEOUT)
+                    ResponseMap::iterator it = responseMap.find(rspId);
+                    if (it != responseMap.end())
                     {
-                        Close();
+                        common::Promise<network::DataBuffer>& rsp = *it->second.Get();
 
-                        return res < 0 ? OperationResult::FAIL : OperationResult::TIMEOUT;
+                        rsp.SetValue(std::auto_ptr<network::DataBuffer>(new network::DataBuffer(msg.Clone())));
+
+                        responseMap.erase(rspId);
                     }
-
-                    sent += res;
                 }
-
-                assert(static_cast<size_t>(sent) == len);
-
-                return OperationResult::SUCCESS;
             }
 
-            bool DataChannel::Receive(interop::InteropMemory& msg, int32_t timeout)
+            void DataChannel::RegisterNotificationHandler(int64_t notId, const SP_NotificationHandler& handler)
             {
-                assert(msg.Capacity() > 4);
+                common::concurrent::CsLockGuard lock(handlerMutex);
 
-                if (socket.get() == 0)
-                    throw IgniteError(IgniteError::IGNITE_ERR_ILLEGAL_STATE, "DataChannel is not established");
-
-                // Message size
-                msg.Length(4);
-
-                OperationResult::T res = ReceiveAll(msg.Data(), static_cast<size_t>(msg.Length()), timeout);
-
-                if (res == OperationResult::TIMEOUT)
-                    return false;
-
-                if (res == OperationResult::FAIL)
-                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, "Can not receive message header");
-
-                interop::InteropInputStream inStream(&msg);
-
-                int32_t msgLen = inStream.ReadInt32();
-
-                if (msgLen < 0)
-                {
-                    Close();
-
-                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                        "Protocol error: Message length is negative");
-                }
-
-                if (msgLen == 0)
-                    return true;
-
-                if (msg.Capacity() < msgLen + 4)
-                    msg.Reallocate(msgLen + 4);
-
-                msg.Length(4 + msgLen);
-
-                res = ReceiveAll(msg.Data() + 4, msgLen, timeout);
-
-                if (res == OperationResult::TIMEOUT)
-                    return false;
-
-                if (res == OperationResult::FAIL)
-                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                        "Connection failure: Can not receive message body");
-
-                return true;
+                NotificationHandlerHolder& holder = handlerMap[notId];
+                holder.SetHandler(handler);
             }
 
-            DataChannel::OperationResult::T DataChannel::ReceiveAll(void* dst, size_t len, int32_t timeout)
+            void DataChannel::DeregisterNotificationHandler(int64_t notId)
             {
-                size_t remain = len;
-                int8_t* buffer = reinterpret_cast<int8_t*>(dst);
+                common::concurrent::CsLockGuard lock(handlerMutex);
 
-                while (remain)
-                {
-                    size_t received = len - remain;
-
-                    int res = socket->Receive(buffer + received, remain, timeout);
-
-                    if (res < 0 || res == network::SocketClient::WaitResult::TIMEOUT)
-                    {
-                        Close();
-
-                        return res < 0 ? OperationResult::FAIL : OperationResult::TIMEOUT;
-                    }
-
-                    remain -= static_cast<size_t>(res);
-                }
-
-                return OperationResult::SUCCESS;
+                handlerMap.erase(notId);
             }
 
-            bool DataChannel::MakeRequestHandshake(const ProtocolVersion& propVer,
-                ProtocolVersion& resVer, int32_t timeout)
+            bool DataChannel::DoHandshake(const ProtocolVersion& propVer)
             {
                 currentVersion = propVer;
 
-                resVer = ProtocolVersion();
-                bool accepted = false;
-
-                try
-                {
-                    // Workaround for some Linux systems that report connection on non-blocking
-                    // sockets as successful but fail to establish real connection.
-                    accepted = Handshake(propVer, resVer, timeout);
-                }
-                catch (const IgniteError&)
-                {
-                    return false;
-                }
-
-                if (!accepted)
-                    return false;
-
-                resVer = propVer;
-
-                return true;
+                return Handshake(propVer);
             }
 
-            bool DataChannel::Handshake(const ProtocolVersion& propVer, ProtocolVersion& resVer, int32_t timeout)
+            bool DataChannel::Handshake(const ProtocolVersion& propVer)
             {
-                // Allocating 4KB just in case.
-                enum { BUFFER_SIZE = 1024 * 4 };
+                // Allocating 4 KB just in case.
+                enum {
+                    BUFFER_SIZE = 1024 * 4
+                };
 
-                common::concurrent::CsLockGuard lock(ioMutex);
-
-                interop::InteropUnpooledMemory mem(BUFFER_SIZE);
-                interop::InteropOutputStream outStream(&mem);
+                interop::SP_InteropMemory mem(new interop::InteropUnpooledMemory(BUFFER_SIZE));
+                interop::InteropOutputStream outStream(mem.Get());
                 binary::BinaryWriterImpl writer(&outStream, 0);
 
                 int32_t lenPos = outStream.Reserve(4);
-                writer.WriteInt8(RequestType::HANDSHAKE);
+                writer.WriteInt8(MessageType::HANDSHAKE);
 
                 writer.WriteInt16(propVer.GetMajor());
                 writer.WriteInt16(propVer.GetMinor());
@@ -297,10 +257,9 @@
 
                 writer.WriteInt8(ClientType::THIN_CLIENT);
 
-                if (propVer >= VERSION_1_7_0)
-                {
+                if (propVer >= VERSION_1_7_0) {
                     // Use features for any new changes in protocol.
-                    int8_t features[] = { 0 };
+                    int8_t features[] = {0};
 
                     writer.WriteInt8Array(features, 0);
                 }
@@ -310,19 +269,17 @@
 
                 outStream.WriteInt32(lenPos, outStream.Position() - 4);
 
-                bool success = Send(mem.Data(), outStream.Position(), timeout);
+                outStream.Synchronize();
 
-                if (!success)
-                    return false;
+                network::DataBuffer buffer(mem);
+                return asyncPool.Get()->Send(id, buffer);
+            }
 
-                success = Receive(mem, timeout);
+            void DataChannel::OnHandshakeResponse(const network::DataBuffer& msg)
+            {
+                interop::InteropInputStream inStream(msg.GetInputStream());
 
-                if (!success)
-                    return false;
-
-                interop::InteropInputStream inStream(&mem);
-
-                inStream.Position(4);
+                inStream.Ignore(4);
 
                 binary::BinaryReaderImpl reader(&inStream);
 
@@ -334,17 +291,33 @@
                     int16_t minor = reader.ReadInt16();
                     int16_t maintenance = reader.ReadInt16();
 
-                    resVer = ProtocolVersion(major, minor, maintenance);
+                    ProtocolVersion resVer(major, minor, maintenance);
 
                     std::string error;
                     reader.ReadString(error);
 
-                    reader.ReadInt32();
+                    int32_t errorCode = reader.ReadInt32();
 
-                    return false;
+                    bool shouldRetry = IsVersionSupported(resVer) && resVer != currentVersion;
+                    if (shouldRetry)
+                        shouldRetry = DoHandshake(resVer);
+
+                    if (!shouldRetry)
+                    {
+                        std::stringstream ss;
+                        ss << errorCode << ": " << error;
+                        std::string newMsg = ss.str();
+
+                        IgniteError err(IgniteError::IGNITE_ERR_GENERIC, newMsg.c_str());
+
+                        if (!handshakePerformed)
+                            stateHandler.OnHandshakeError(id, err);
+                    }
+
+                    return;
                 }
 
-                if (propVer >= VERSION_1_7_0)
+                if (currentVersion >= VERSION_1_7_0)
                 {
                     int32_t len = reader.ReadInt8Array(0, 0);
                     std::vector<int8_t> features;
@@ -356,75 +329,82 @@
                     }
                 }
 
-                if (propVer >= VERSION_1_4_0)
+                if (currentVersion >= VERSION_1_4_0)
                 {
                     Guid nodeGuid = reader.ReadGuid();
 
                     node.SetGuid(nodeGuid);
                 }
 
-                return true;
-            }
-
-            bool DataChannel::EnsureConnected(int32_t timeout)
-            {
-                if (socket.get() != 0)
-                    return true;
-
-                return TryRestoreConnection(timeout);
-            }
-
-            bool DataChannel::NegotiateProtocolVersion(int32_t timeout)
-            {
-                ProtocolVersion resVer;
-                ProtocolVersion propVer = VERSION_DEFAULT;
-
-                bool success = MakeRequestHandshake(propVer, resVer, timeout);
-
-                while (!success)
-                {
-                    if (resVer == propVer || !IsVersionSupported(resVer))
-                        return false;
-
-                    propVer = resVer;
-
-                    success = MakeRequestHandshake(propVer, resVer, timeout);
-                }
-
-                return true;
-            }
-
-            bool DataChannel::TryRestoreConnection(int32_t timeout)
-            {
-                bool connected = false;
-
-                const network::EndPoint& endPoint = node.GetEndPoint();
-
-                connected = socket->Connect(endPoint.host.c_str(), endPoint.port, timeout);
-
-                if (!connected)
-                {
-                    Close();
-
-                    return false;
-                }
-
-                connected = NegotiateProtocolVersion(timeout);
-
-                if (!connected)
-                {
-                    Close();
-
-                    return false;
-                }
-
-                return true;
+                handshakePerformed = true;
+                stateHandler.OnHandshakeSuccess(id);
             }
 
             bool DataChannel::IsVersionSupported(const ProtocolVersion& ver)
             {
                 return supportedVersions.find(ver) != supportedVersions.end();
             }
+
+            void DataChannel::DeserializeMessage(const network::DataBuffer &data, Response &msg)
+            {
+                interop::InteropInputStream inStream(data.GetInputStream());
+
+                // Skipping size (4 bytes) and reqId (8 bytes)
+                inStream.Ignore(12);
+
+                binary::BinaryReaderImpl reader(&inStream);
+
+                msg.Read(reader, currentVersion);
+            }
+
+            void DataChannel::FailPendingRequests(const IgniteError* err)
+            {
+                IgniteError defaultErr(IgniteError::IGNITE_ERR_NETWORK_FAILURE, "Connection was closed");
+                if (!err)
+                    err = &defaultErr;
+
+                {
+                    common::concurrent::CsLockGuard lock(responseMutex);
+
+                    for (ResponseMap::iterator it = responseMap.begin(); it != responseMap.end(); ++it)
+                        it->second.Get()->SetError(*err);
+
+                    responseMap.clear();
+                }
+
+                {
+                    common::concurrent::CsLockGuard lock(handlerMutex);
+
+                    for (NotificationHandlerMap::iterator it = handlerMap.begin(); it != handlerMap.end(); ++it)
+                    {
+                        common::SP_ThreadPoolTask task = it->second.ProcessClosed();
+
+                        if (task.IsValid())
+                            userThreadPool.Dispatch(task);
+                    }
+                }
+
+                if (!handshakePerformed)
+                    stateHandler.OnHandshakeError(id, *err);
+            }
+
+            void DataChannel::CloseResource(int64_t resourceId)
+            {
+                ResourceCloseRequest req(resourceId);
+                Response rsp;
+
+                try
+                {
+                    SyncMessage(req, rsp, config.GetConnectionTimeout());
+                }
+                catch (const IgniteError& err)
+                {
+                    // Network failure means connection is closed or broken, which means
+                    // that all resources were freed automatically.
+                    if (err.GetCode() != IgniteError::IGNITE_ERR_NETWORK_FAILURE)
+                        throw;
+                }
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/thin-client/src/impl/data_channel.h b/modules/platforms/cpp/thin-client/src/impl/data_channel.h
index 38f0180..5e09b50 100644
--- a/modules/platforms/cpp/thin-client/src/impl/data_channel.h
+++ b/modules/platforms/cpp/thin-client/src/impl/data_channel.h
@@ -22,10 +22,13 @@
 
 #include <memory>
 
+#include <ignite/future.h>
 #include <ignite/thin/ignite_client_configuration.h>
 
 #include <ignite/common/concurrent.h>
+#include <ignite/common/thread_pool.h>
 #include <ignite/network/socket_client.h>
+#include <ignite/network/async_client_pool.h>
 
 #include <ignite/impl/interop/interop_output_stream.h>
 #include <ignite/impl/binary/binary_writer_impl.h>
@@ -33,6 +36,8 @@
 #include "impl/protocol_version.h"
 #include "impl/ignite_node.h"
 #include "impl/response_status.h"
+#include "impl/channel_state_handler.h"
+#include "impl/notification_handler.h"
 
 namespace ignite
 {
@@ -46,6 +51,12 @@
 
         namespace thin
         {
+            // Forward declaration.
+            class Request;
+
+            // Forward declaration.
+            class Response;
+
             /**
              * Data router.
              *
@@ -58,12 +69,21 @@
                 /** Version set type. */
                 typedef std::set<ProtocolVersion> VersionSet;
 
+                /** Shared pointer to DataBuffer Promise. */
+                typedef common::concurrent::SharedPointer<common::Promise<network::DataBuffer> > SP_PromiseDataBuffer;
+
+                /** Response map. */
+                typedef std::map< int64_t, SP_PromiseDataBuffer> ResponseMap;
+
+                /** Notification handler map. */
+                typedef std::map< int64_t, NotificationHandlerHolder > NotificationHandlerMap;
+
                 /** Version 1.2.0. */
                 static const ProtocolVersion VERSION_1_2_0;
 
                 /** Version 1.3.0. */
                 static const ProtocolVersion VERSION_1_3_0;
-                
+
                 /** Version 1.4.0. Added: Partition awareness support, IEP-23. */
                 static const ProtocolVersion VERSION_1_4_0;
 
@@ -73,32 +93,30 @@
                 /** Version 1.6.0. Expiration Policy Configuration. */
                 static const ProtocolVersion VERSION_1_6_0;
 
-                /** Version 1.6.0. Features introduced. */
+                /** Version 1.7.0. Features introduced. */
                 static const ProtocolVersion VERSION_1_7_0;
 
                 /** Current version. */
                 static const ProtocolVersion VERSION_DEFAULT;
 
                 /**
-                 * Operation with timeout result.
-                 */
-                struct OperationResult
-                {
-                    enum T
-                    {
-                        SUCCESS,
-                        FAIL,
-                        TIMEOUT
-                    };
-                };
-
-                /**
                  * Constructor.
                  *
+                 * @param id Connection ID.
+                 * @param addr Address.
+                 * @param asyncPool Async pool for connection.
                  * @param cfg Configuration.
                  * @param typeMgr Type manager.
+                 * @param stateHandler State handler.
+                 * @param userThreadPool Thread pool to use to dispatch tasks that can run user code.
                  */
-                DataChannel(const ignite::thin::IgniteClientConfiguration& cfg, binary::BinaryTypeManager& typeMgr);
+                DataChannel(uint64_t id,
+                    const network::EndPoint& addr,
+                    const ignite::network::SP_AsyncClientPool& asyncPool,
+                    const ignite::thin::IgniteClientConfiguration& cfg,
+                    binary::BinaryTypeManager& typeMgr,
+                    ChannelStateHandler& stateHandler,
+                    common::ThreadPool& userThreadPool);
 
                 /**
                  * Destructor.
@@ -106,137 +124,50 @@
                 ~DataChannel();
 
                 /**
-                 * Establish connection to cluster.
+                 * Perform handshake.
                  *
-                 * @param host Host.
-                 * @param port Port.
-                 * @param timeout Timeout.
                  * @return @c true on success.
                  */
-                bool Connect(const std::string& host, uint16_t port, int32_t timeout);
+                void StartHandshake();
 
                 /**
                  * Close connection.
+                 *
+                 * @param err Error.
                  */
-                void Close();
+                void Close(const IgniteError* err);
 
                 /**
-                 * Synchronously send request message and receive response.
-                 * Uses provided timeout. Does not try to restore connection on
-                 * fail.
+                 * Synchronously send request message and receive response. Uses provided timeout.
                  *
                  * @param req Request message.
                  * @param rsp Response message.
                  * @param timeout Timeout.
                  * @throw IgniteError on error.
                  */
-                template<typename ReqT, typename RspT>
-                void SyncMessage(const ReqT& req, RspT& rsp, int32_t timeout)
-                {
-                    // Allocating 64KB to lessen number of re-allocations.
-                    enum { BUFFER_SIZE = 1024 * 64 };
-
-                    interop::InteropUnpooledMemory mem(BUFFER_SIZE);
-
-                    int64_t id = GenerateRequestMessage(req, mem);
-
-                    InternalSyncMessage(mem, timeout);
-
-                    interop::InteropInputStream inStream(&mem);
-
-                    inStream.Position(4);
-
-                    int64_t rspId = inStream.ReadInt64();
-
-                    if (id != rspId)
-                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                            "Protocol error: Response message ID does not equal Request ID");
-
-                    binary::BinaryReaderImpl reader(&inStream);
-
-                    rsp.Read(reader, currentVersion);
-                }
+                void SyncMessage(Request& req, Response& rsp, int32_t timeout);
 
                 /**
-                 * Synchronously send request message, receive response and get a notification.
+                 * Process received message.
                  *
-                 * @param req Request message.
-                 * @param notification Notification message.
-                 * @param timeout Timeout.
-                 * @return Channel that was used for request.
-                 * @throw IgniteError on error.
+                 * @param msg Message.
                  */
-                template<typename ReqT, typename NotT>
-                void SyncMessageWithNotification(const ReqT& req, NotT& notification, int32_t timeout)
-                {
-                    // Allocating 64KB to lessen number of re-allocations.
-                    enum { BUFFER_SIZE = 1024 * 64 };
-
-                    interop::InteropUnpooledMemory mem(BUFFER_SIZE);
-
-                    int64_t id = GenerateRequestMessage(req, mem);
-
-                    common::concurrent::CsLockGuard lock(ioMutex);
-
-                    InternalSyncMessageUnguarded(mem, timeout);
-
-                    interop::InteropInputStream inStream(&mem);
-
-                    inStream.Position(4);
-
-                    int64_t rspId = inStream.ReadInt64();
-
-                    if (id != rspId)
-                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
-                            "Protocol error: Response message ID does not equal Request ID");
-
-                    binary::BinaryReaderImpl reader(&inStream);
-
-                    typedef typename NotT::ResponseType RspT;
-                    RspT rsp;
-
-                    rsp.Read(reader, currentVersion);
-
-                    if (rsp.GetStatus() != ResponseStatus::SUCCESS)
-                        throw IgniteError(IgniteError::IGNITE_ERR_COMPUTE_EXECUTION_REJECTED, rsp.GetError().c_str());
-
-                    bool success = Receive(mem, 0);
-
-                    if (!success)
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                            "Can not receive message response from the remote host: timeout");
-
-                    inStream.Position(4);
-                    inStream.Synchronize();
-
-                    int64_t notificationId = inStream.ReadInt64();
-
-                    if (notificationId != rsp.GetNotificationId())
-                    {
-                        IGNITE_ERROR_FORMATTED_2(IgniteError::IGNITE_ERR_GENERIC, "Unexpected notification ID",
-                            "expected", rsp.GetNotificationId(), "actual", notificationId)
-                    }
-
-                    notification.Read(reader, currentVersion);
-                }
+                void ProcessMessage(const network::DataBuffer& msg);
 
                 /**
-                 * Send message stored in memory and synchronously receives
-                 * response and stores it in the same memory.
+                 * Register handler for the notification.
                  *
-                 * @param mem Memory.
-                 * @param timeout Operation timeout.
+                 * @param notId Notification ID.
+                 * @param handler Handler.
                  */
-                void InternalSyncMessage(interop::InteropUnpooledMemory& mem, int32_t timeout);
+                void RegisterNotificationHandler(int64_t notId, const SP_NotificationHandler& handler);
 
                 /**
-                 * Send message stored in memory and synchronously receives
-                 * response and stores it in the same memory.
+                 * Deregister handler for the notification.
                  *
-                 * @param mem Memory.
-                 * @param timeout Operation timeout.
+                 * @param notId Notification ID.
                  */
-                void InternalSyncMessageUnguarded(interop::InteropUnpooledMemory& mem, int32_t timeout);
+                void DeregisterNotificationHandler(int64_t notId);
 
                 /**
                  * Get remote node.
@@ -248,15 +179,35 @@
                 }
 
                 /**
-                 * Check if the connection established.
-                 *
-                 * @return @true if connected.
+                 * Get connection ID.
+                 * @return Connection ID.
                  */
-                bool IsConnected() const
+                uint64_t GetId() const
                 {
-                    return socket.get() != 0;
+                    return id;
                 }
 
+                /**
+                 * Deserialize message received by this channel.
+                 * @param data Data.
+                 * @param msg Message.
+                 */
+                void DeserializeMessage(const network::DataBuffer& data, Response& msg);
+
+                /**
+                 * Fail all pending requests.
+                 *
+                 * @param err Error.
+                 */
+                void FailPendingRequests(const IgniteError* err);
+
+                /**
+                 * Close remote resource.
+                 *
+                 * @param resourceId Resource ID.
+                 */
+                void CloseResource(int64_t resourceId);
+
             private:
                 IGNITE_NO_COPY_ASSIGNMENT(DataChannel);
 
@@ -279,115 +230,40 @@
                  * @param mem Memory to write request to.
                  * @return Message ID.
                  */
-                template<typename ReqT>
-                int64_t GenerateRequestMessage(const ReqT& req, interop::InteropUnpooledMemory& mem)
-                {
-                    interop::InteropOutputStream outStream(&mem);
-                    binary::BinaryWriterImpl writer(&outStream, &typeMgr);
-
-                    // Space for RequestSize + OperationCode + RequestID.
-                    outStream.Reserve(4 + 2 + 8);
-
-                    req.Write(writer, currentVersion);
-
-                    int64_t id = GenerateRequestId();
-
-                    outStream.WriteInt32(0, outStream.Position() - 4);
-                    outStream.WriteInt16(4, ReqT::GetOperationCode());
-                    outStream.WriteInt64(6, id);
-
-                    outStream.Synchronize();
-
-                    return id;
-                }
+                int64_t GenerateRequestMessage(Request& req, interop::InteropMemory& mem);
 
                 /**
-                 * Send data by established connection.
+                 * Asynchronously send request message and get a future for the response.
                  *
-                 * @param data Data buffer.
-                 * @param len Data length.
-                 * @param timeout Timeout.
-                 * @return @c true on success, @c false on timeout.
+                 * @param req Request message.
                  * @throw IgniteError on error.
                  */
-                bool Send(const int8_t* data, size_t len, int32_t timeout);
-
-                /**
-                 * Receive next message.
-                 *
-                 * @param msg Buffer for message.
-                 * @param timeout Timeout.
-                 * @return @c true on success, @c false on timeout.
-                 * @throw IgniteError on error.
-                 */
-                bool Receive(interop::InteropMemory& msg, int32_t timeout);
-
-                /**
-                 * Receive specified number of bytes.
-                 *
-                 * @param dst Buffer for data.
-                 * @param len Number of bytes to receive.
-                 * @param timeout Timeout.
-                 * @return Operation result.
-                 */
-                OperationResult::T ReceiveAll(void* dst, size_t len, int32_t timeout);
-
-                /**
-                 * Send specified number of bytes.
-                 *
-                 * @param data Data buffer.
-                 * @param len Data length.
-                 * @param timeout Timeout.
-                 * @return Operation result.
-                 */
-                OperationResult::T SendAll(const int8_t* data, size_t len, int32_t timeout);
+                Future<network::DataBuffer> AsyncMessage(Request &req);
 
                 /**
                  * Perform handshake request.
                  *
                  * @param propVer Proposed protocol version.
-                 * @param resVer Resulted version.
-                 * @param timeout Timeout.
                  * @return @c true on success and @c false otherwise.
                  */
-                bool MakeRequestHandshake(const ProtocolVersion& propVer, ProtocolVersion& resVer, int32_t timeout);
+                bool DoHandshake(const ProtocolVersion& propVer);
 
                 /**
-                 * Synchronously send handshake request message and receive
-                 * handshake response. Uses provided timeout. Does not try to
-                 * restore connection on fail.
+                 * Synchronously send handshake request message and receive handshake response. Uses provided timeout.
+                 * Does not try to restore connection on fail.
                  *
                  * @param propVer Proposed protocol version.
-                 * @param resVer Resulted version.
-                 * @param timeout Timeout.
                  * @return @c true if accepted.
                  * @throw IgniteError on error.
                  */
-                bool Handshake(const ProtocolVersion& propVer, ProtocolVersion& resVer, int32_t timeout);
+                bool Handshake(const ProtocolVersion& propVer);
 
                 /**
-                 * Ensure there is a connection to the cluster.
+                 * Handle handshake response.
                  *
-                 * @param timeout Timeout.
-                 * @return @c false on error.
+                 * @param msg Message.
                  */
-                bool EnsureConnected(int32_t timeout);
-
-                /**
-                 * Negotiate protocol version with current host
-                 *
-                 * @param timeout Timeout.
-                 * @return @c true on success and @c false otherwise.
-                 */
-                bool NegotiateProtocolVersion(int32_t timeout);
-
-                /**
-                 * Try to restore connection to the cluster.
-                 *
-                 * @param timeout Timeout.
-                 * @return @c true on success and @c false otherwise.
-                 */
-                bool TryRestoreConnection(int32_t timeout);
+                void OnHandshakeResponse(const network::DataBuffer& msg);
 
                 /**
                  * Check if the version is supported.
@@ -400,8 +276,17 @@
                 /** Set of supported versions. */
                 const static VersionSet supportedVersions;
 
-                /** Sync IO mutex. */
-                common::concurrent::CriticalSection ioMutex;
+                /** State handler. */
+                ChannelStateHandler& stateHandler;
+
+                /** Indicates whether handshake has been performed. */
+                bool handshakePerformed;
+
+                /** Connection ID */
+                uint64_t id;
+
+                /** Async pool. */
+                ignite::network::SP_AsyncClientPool asyncPool;
 
                 /** Remote node data. */
                 IgniteNode node;
@@ -418,8 +303,20 @@
                 /** Request ID counter. */
                 int64_t reqIdCounter;
 
-                /** Client Socket. */
-                std::auto_ptr<network::SocketClient> socket;
+                /** Response map mutex. */
+                common::concurrent::CriticalSection responseMutex;
+
+                /** Responses. */
+                ResponseMap responseMap;
+
+                /** Notification handlers mutex. */
+                common::concurrent::CriticalSection handlerMutex;
+
+                /** Notification handlers. */
+                NotificationHandlerMap handlerMap;
+
+                /** Thread pool to dispatch user code execution. */
+                common::ThreadPool& userThreadPool;
             };
 
             /** Shared pointer type. */
diff --git a/modules/platforms/cpp/thin-client/src/impl/data_router.cpp b/modules/platforms/cpp/thin-client/src/impl/data_router.cpp
index 0943afa..78580ca 100644
--- a/modules/platforms/cpp/thin-client/src/impl/data_router.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/data_router.cpp
@@ -19,11 +19,14 @@
 #include <cstddef>
 #include <cstdlib>
 
-#include <sstream>
 #include <iterator>
 #include <algorithm>
 
+#include <ignite/network/codec_data_filter.h>
+#include <ignite/network/length_prefix_codec.h>
+#include <ignite/network/network.h>
 #include <ignite/network/utils.h>
+#include <ignite/network/ssl/secure_data_filter.h>
 
 #include "impl/utility.h"
 #include "impl/data_router.h"
@@ -38,9 +41,8 @@
         namespace thin
         {
             DataRouter::DataRouter(const ignite::thin::IgniteClientConfiguration& cfg) :
-                ioTimeout(DEFAULT_IO_TIMEOUT),
-                connectionTimeout(DEFAULT_CONNECT_TIMEOUT),
-                config(cfg)
+                config(cfg),
+                userThreadPool(0)
             {
                 srand(common::GetRandSeed());
 
@@ -53,7 +55,7 @@
 
             DataRouter::~DataRouter()
             {
-                // No-op.
+                Close();
             }
 
             void DataRouter::Connect()
@@ -63,80 +65,208 @@
                 if (ranges.empty())
                     throw IgniteError(IgniteError::IGNITE_ERR_ILLEGAL_ARGUMENT, "No valid address to connect.");
 
-                ChannelsVector newLegacyChannels;
-                newLegacyChannels.reserve(ranges.size());
-
-                for (std::vector<network::TcpRange>::iterator it = ranges.begin(); it != ranges.end(); ++it)
+                if (!asyncPool.IsValid())
                 {
-                    network::TcpRange& range = *it;
+                    std::vector<network::SP_DataFilter> filters;
 
-                    for (uint16_t port = range.port; port <= range.port + range.range; ++port)
+                    if (config.GetSslMode() == SslMode::REQUIRE)
                     {
-                        SP_DataChannel channel(new DataChannel(config, typeMgr));
+                        network::ssl::EnsureSslLoaded();
 
-                        bool connected = false;
+                        network::ssl::SecureConfiguration sslCfg;
+                        sslCfg.caPath = config.GetSslCaFile();
+                        sslCfg.keyPath = config.GetSslKeyFile();
+                        sslCfg.certPath = config.GetSslCertFile();
 
-                        try
-                        {
-                            connected = channel.Get()->Connect(range.host, port, connectionTimeout);
-                        }
-                        catch (const IgniteError&)
-                        {
-                            // No-op.
-                        }
-
-                        if (connected)
-                        {
-                            const IgniteNode& newNode = channel.Get()->GetNode();
-
-                            if (newNode.IsLegacy())
-                            {
-                                newLegacyChannels.push_back(channel);
-                            }
-                            else
-                            {
-                                common::concurrent::CsLockGuard lock(channelsMutex);
-
-                                // Insertion takes place if no channel with the GUID is already present.
-                                std::pair<ChannelsGuidMap::iterator, bool> res =
-                                    channels.insert(std::make_pair(newNode.GetGuid(), channel));
-
-                                bool inserted = res.second;
-                                SP_DataChannel& oldChannel = res.first->second;
-
-                                if (!inserted && !oldChannel.Get()->IsConnected())
-                                    oldChannel.Swap(channel);
-                            }
-
-                            break;
-                        }
+                        network::ssl::SP_SecureDataFilter secureFilter(new network::ssl::SecureDataFilter(sslCfg));
+                        filters.push_back(secureFilter);
                     }
 
-                    if (config.GetConnectionsLimit())
-                    {
-                        common::concurrent::CsLockGuard lock(channelsMutex);
+                    network::SP_CodecFactory codecFactory(new network::LengthPrefixCodecFactory());
+                    network::SP_CodecDataFilter codecFilter(new network::CodecDataFilter(codecFactory));
+                    filters.push_back(codecFilter);
 
-                        size_t connectionsNum = newLegacyChannels.size() + channels.size();
+                    asyncPool = network::MakeAsyncClientPool(filters);
 
-                        if (connectionsNum >= config.GetConnectionsLimit())
-                            break;
-                    }
+                    if (!asyncPool.IsValid())
+                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Can not create async connection pool");
+
+                    asyncPool.Get()->SetHandler(this);
                 }
 
-                common::concurrent::CsLockGuard lock(channelsMutex);
+                userThreadPool.Start();
+                asyncPool.Get()->Start(ranges, config.GetConnectionsLimit());
 
-                legacyChannels.swap(newLegacyChannels);
+                bool connected = EnsureConnected(config.GetConnectionTimeout());
 
-                if (channels.empty() && legacyChannels.empty())
-                    throw IgniteError(IgniteError::IGNITE_ERR_GENERIC, "Failed to establish connection with any host.");
+                if (!connected)
+                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
+                        "Failed to establish connection with any host.");
             }
 
             void DataRouter::Close()
             {
+                if (asyncPool.IsValid())
+                {
+                    asyncPool.Get()->SetHandler(0);
+                    asyncPool.Get()->Stop();
+                }
+
+                userThreadPool.Stop();
+            }
+
+            bool DataRouter::EnsureConnected(int32_t timeout)
+            {
                 common::concurrent::CsLockGuard lock(channelsMutex);
 
-                channels.clear();
-                legacyChannels.clear();
+                if (!connectedChannels.empty())
+                    return true;
+
+                CheckHandshakeErrorLocked();
+
+                channelsWaitPoint.WaitFor(channelsMutex, timeout);
+
+                CheckHandshakeErrorLocked();
+
+                return !connectedChannels.empty();
+            }
+
+            void DataRouter::CheckHandshakeErrorLocked()
+            {
+                if (!lastHandshakeError.get())
+                    return;
+
+                IgniteError err = *lastHandshakeError;
+                lastHandshakeError.reset();
+
+                throw IgniteError(err);
+            }
+
+            void DataRouter::OnConnectionSuccess(const network::EndPoint& addr, uint64_t id)
+            {
+                SP_DataChannel channel(new DataChannel(id, addr, asyncPool, config, typeMgr, *this, userThreadPool));
+
+                {
+                    common::concurrent::CsLockGuard lock(channelsMutex);
+
+                    channels[id] = channel;
+                }
+
+                channel.Get()->StartHandshake();
+            }
+
+            void DataRouter::OnConnectionError(const network::EndPoint& addr, const IgniteError& err)
+            {
+                IGNITE_UNUSED(addr);
+
+                if (!connectedChannels.empty())
+                    return;
+
+                if (err.GetCode() != IgniteError::IGNITE_ERR_SECURE_CONNECTION_FAILURE)
+                    return;
+
+                common::concurrent::CsLockGuard lock(channelsMutex);
+
+                lastHandshakeError.reset(new IgniteError(err));
+                channelsWaitPoint.NotifyAll();
+            }
+
+            void DataRouter::OnConnectionClosed(uint64_t id, const IgniteError* err)
+            {
+                SP_DataChannel channel;
+                {
+                    common::concurrent::CsLockGuard lock(channelsMutex);
+
+                    channel = FindChannelLocked(id);
+
+                    connectedChannels.erase(id);
+                    InvalidateChannelLocked(channel);
+                }
+
+                channel.Get()->FailPendingRequests(err);
+            }
+
+            void DataRouter::OnMessageReceived(uint64_t id, const network::DataBuffer& msg)
+            {
+                SP_DataChannel channel = FindChannel(id);
+
+                if (channel.IsValid())
+                    channel.Get()->ProcessMessage(msg);
+            }
+
+            void DataRouter::OnMessageSent(uint64_t id)
+            {
+                IGNITE_UNUSED(id);
+                // No-op.
+            }
+
+            void DataRouter::OnHandshakeSuccess(uint64_t id)
+            {
+                common::concurrent::CsLockGuard lock(channelsMutex);
+
+                connectedChannels.insert(id);
+                channelsWaitPoint.NotifyAll();
+
+                SP_DataChannel channel = FindChannelLocked(id);
+                if (channel.IsValid())
+                {
+                    const IgniteNode& node = channel.Get()->GetNode();
+                    if (!node.IsLegacy())
+                        partChannels[node.GetGuid()] = channel;
+                }
+            }
+
+            void DataRouter::OnHandshakeError(uint64_t id, const IgniteError& err)
+            {
+                IGNITE_UNUSED(id);
+
+                common::concurrent::CsLockGuard lock(channelsMutex);
+
+                lastHandshakeError.reset(new IgniteError(err));
+                channelsWaitPoint.NotifyAll();
+            }
+
+            void DataRouter::OnNotificationHandlingError(uint64_t id, const IgniteError &err)
+            {
+                SP_DataChannel channel = FindChannel(id);
+
+                if (channel.IsValid())
+                    channel.Get()->Close(&err);
+            }
+
+            SP_DataChannel DataRouter::SyncMessage(Request &req, Response &rsp)
+            {
+                SP_DataChannel channel = GetRandomChannel();
+
+                int32_t metaVer = typeMgr.GetVersion();
+
+                channel = SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
+
+                ProcessMeta(metaVer);
+
+                return channel;
+            }
+
+            SP_DataChannel DataRouter::SyncMessage(Request &req, Response &rsp, const Guid &hint)
+            {
+                SP_DataChannel channel = GetBestChannel(hint);
+
+                int32_t metaVer = typeMgr.GetVersion();
+
+                channel = SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
+
+                ProcessMeta(metaVer);
+
+                return channel;
+            }
+
+            SP_DataChannel DataRouter::SyncMessageNoMetaUpdate(Request &req, Response &rsp)
+            {
+                SP_DataChannel channel = GetRandomChannel();
+
+                channel = SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
+
+                return channel;
             }
 
             void DataRouter::ProcessMeta(int32_t metaVer)
@@ -150,6 +280,55 @@
                 }
             }
 
+            void DataRouter::CheckAffinity(Response &rsp)
+            {
+                const AffinityTopologyVersion* ver = rsp.GetAffinityTopologyVersion();
+
+                if (ver != 0 && config.IsPartitionAwareness())
+                    affinityManager.UpdateAffinity(*ver);
+            }
+
+            SP_DataChannel DataRouter::SyncMessagePreferredChannelNoMetaUpdate(Request &req, Response &rsp,
+                const SP_DataChannel &preferred)
+            {
+                SP_DataChannel channel(preferred);
+
+                if (!channel.IsValid())
+                    channel = GetRandomChannel();
+
+                if (!channel.IsValid())
+                {
+                    bool connected = EnsureConnected(config.GetConnectionTimeout());
+
+                    if (!connected)
+                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
+                            "Failed to establish connection with any host.");
+
+                    channel = GetRandomChannel();
+                    if (!channel.IsValid())
+                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
+                            "Failed to establish connection with any host.");
+                }
+
+                try
+                {
+                    channel.Get()->SyncMessage(req, rsp, config.GetConnectionTimeout());
+                }
+                catch (IgniteError& err)
+                {
+                    InvalidateChannel(channel);
+
+                    std::string msg("Connection failure during command processing. Please re-run command. Cause: ");
+                    msg += err.GetText();
+
+                    throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
+                }
+
+                CheckAffinity(rsp);
+
+                return channel;
+            }
+
             void DataRouter::RefreshAffinityMapping(int32_t cacheId)
             {
                 std::vector<int32_t> ids(1, cacheId);
@@ -182,86 +361,54 @@
                 if (!channel.IsValid())
                     return;
 
-                const IgniteNode& node = channel.Get()->GetNode();
-
                 common::concurrent::CsLockGuard lock(channelsMutex);
 
-                if (!node.IsLegacy())
-                {
-                    channels.erase(node.GetGuid());
-                }
-                else
-                {
-                    const network::EndPoint& ep1 = node.GetEndPoint();
-                    for (ChannelsVector::iterator it = legacyChannels.begin(); it != legacyChannels.end(); ++it)
-                    {
-                        const network::EndPoint& ep2 = it->Get()->GetNode().GetEndPoint();
+                InvalidateChannelLocked(channel);
+            }
 
-                        if (ep1 == ep2)
-                        {
-                            legacyChannels.erase(it);
+            void DataRouter::InvalidateChannelLocked(SP_DataChannel &channel)
+            {
+                if (!channel.IsValid())
+                    return;
 
-                            break;
-                        }
-                    }
-                }
-
-                channel = SP_DataChannel();
+                DataChannel& channel0 = *channel.Get();
+                channels.erase(channel0.GetId());
+                partChannels.erase(channel0.GetNode().GetGuid());
             }
 
             SP_DataChannel DataRouter::GetRandomChannel()
             {
                 common::concurrent::CsLockGuard lock(channelsMutex);
 
-                return GetRandomChannelUnsafe();
+                return GetRandomChannelLocked();
             }
 
-            SP_DataChannel DataRouter::GetRandomChannelUnsafe()
+            SP_DataChannel DataRouter::GetRandomChannelLocked()
             {
-                if (channels.empty() && legacyChannels.empty())
+                if (connectedChannels.empty())
                     return SP_DataChannel();
 
                 int r = rand();
 
-                size_t idx = r % (channels.size() + legacyChannels.size());
+                size_t idx = r % connectedChannels.size();
 
-                if (idx >= channels.size())
-                {
-                    size_t legacyIdx = idx - channels.size();
-
-                    return legacyChannels[legacyIdx];
-                }
-
-                ChannelsGuidMap::iterator it = channels.begin();
+                ChannelsIdSet::iterator it = connectedChannels.begin();
 
                 std::advance(it, idx);
 
-                return it->second;
-            }
-
-            bool DataRouter::IsProvidedByUser(const network::EndPoint& endPoint)
-            {
-                for (std::vector<network::TcpRange>::iterator it = ranges.begin(); it != ranges.end(); ++it)
-                {
-                    if (it->host == endPoint.host &&
-                        endPoint.port >= it->port &&
-                        endPoint.port <= it->port + it->range)
-                        return true;
-                }
-
-                return false;
+                return channels[*it];
             }
 
             SP_DataChannel DataRouter::GetBestChannel(const Guid& hint)
             {
                 common::concurrent::CsLockGuard lock(channelsMutex);
 
-                ChannelsGuidMap::iterator itChannel = channels.find(hint);
+                ChannelsGuidMap::iterator itChannel = partChannels.find(hint);
 
-                if (itChannel != channels.end())
+                if (itChannel != partChannels.end())
                     return itChannel->second;
 
-                return GetRandomChannelUnsafe();
+                return GetRandomChannelLocked();
             }
 
             void DataRouter::CollectAddresses(const std::string& str, std::vector<network::TcpRange>& ranges)
@@ -272,6 +419,21 @@
 
                 std::random_shuffle(ranges.begin(), ranges.end());
             }
+
+            SP_DataChannel DataRouter::FindChannel(uint64_t id)
+            {
+                common::concurrent::CsLockGuard lock(channelsMutex);
+                return FindChannelLocked(id);
+            }
+
+            SP_DataChannel DataRouter::FindChannelLocked(uint64_t id)
+            {
+                ChannelsIdMap::iterator it = channels.find(id);
+                if (it != channels.end())
+                    return it->second;
+
+                return SP_DataChannel();
+            }
         }
     }
 }
diff --git a/modules/platforms/cpp/thin-client/src/impl/data_router.h b/modules/platforms/cpp/thin-client/src/impl/data_router.h
index 92ac43e..aa31f39 100644
--- a/modules/platforms/cpp/thin-client/src/impl/data_router.h
+++ b/modules/platforms/cpp/thin-client/src/impl/data_router.h
@@ -29,12 +29,16 @@
 #include <ignite/thin/ignite_client_configuration.h>
 
 #include <ignite/common/concurrent.h>
+#include <ignite/common/thread_pool.h>
+#include <ignite/common/promise.h>
 #include <ignite/network/end_point.h>
 #include <ignite/network/tcp_range.h>
+#include <ignite/network/async_client_pool.h>
 #include <ignite/impl/binary/binary_writer_impl.h>
 
 #include "impl/affinity/affinity_assignment.h"
 #include "impl/affinity/affinity_manager.h"
+#include "impl/channel_state_handler.h"
 #include "impl/data_channel.h"
 
 namespace ignite
@@ -46,24 +50,25 @@
             // Forward declaration.
             class WritableKey;
 
+            // Forward declaration.
+            class Request;
+
+            // Forward declaration.
+            class Response;
+
             /**
              * Data router.
              *
              * Ensures there is a connection between client and one of the servers
              * and routes data between them.
              */
-            class DataRouter
+            class DataRouter : public network::AsyncHandler, public ChannelStateHandler
             {
                 typedef std::map<Guid, SP_DataChannel> ChannelsGuidMap;
-                typedef std::vector<SP_DataChannel> ChannelsVector;
+                typedef std::map<uint64_t, SP_DataChannel> ChannelsIdMap;
+                typedef std::set<uint64_t> ChannelsIdSet;
 
             public:
-                /** Connection establishment timeout in seconds. */
-                enum { DEFAULT_CONNECT_TIMEOUT = 5 };
-
-                /** Network IO operation timeout in seconds. */
-                enum { DEFAULT_IO_TIMEOUT = 5 };
-
                 /** Default port. */
                 enum { DEFAULT_PORT = 10800 };
 
@@ -90,25 +95,66 @@
                 void Close();
 
                 /**
-                 * Update affinity if needed.
+                 * Callback that called on successful connection establishment.
                  *
-                 * @param rsp Response.
+                 * @param addr Address of the new connection.
+                 * @param id Connection ID.
                  */
-                template <typename RspT>
-                void CheckAffinity(RspT& rsp)
-                {
-                    const AffinityTopologyVersion* ver = rsp.GetAffinityTopologyVersion();
-
-                    if (ver != 0 && config.IsPartitionAwareness())
-                        affinityManager.UpdateAffinity(*ver);
-                }
+                virtual void OnConnectionSuccess(const network::EndPoint& addr, uint64_t id);
 
                 /**
-                 * Process meta if needed.
+                 * Callback that called on error during connection establishment.
                  *
-                 * @param metaVer Version of meta.
+                 * @param addr Connection address.
+                 * @param err Error.
                  */
-                void ProcessMeta(int32_t metaVer);
+                virtual void OnConnectionError(const network::EndPoint& addr, const IgniteError& err);
+
+                /**
+                 * Callback that called on error during connection establishment.
+                 *
+                 * @param id Async client ID.
+                 * @param err Error.
+                 */
+                virtual void OnConnectionClosed(uint64_t id, const IgniteError* err);
+
+                /**
+                 * Callback that called when new message is received.
+                 *
+                 * @param id Async client ID.
+                 * @param msg Received message.
+                 */
+                virtual void OnMessageReceived(uint64_t id, const network::DataBuffer& msg);
+
+                /**
+                 * Callback that called when message is sent.
+                 *
+                 * @param id Async client ID.
+                 */
+                virtual void OnMessageSent(uint64_t id);
+
+                /**
+                 * Channel handshake completion callback.
+                 *
+                 * @param id Channel ID.
+                 */
+                virtual void OnHandshakeSuccess(uint64_t id);
+
+                /**
+                 * Channel handshake error callback.
+                 *
+                 * @param id Channel ID.
+                 * @param err Error.
+                 */
+                virtual void OnHandshakeError(uint64_t id, const IgniteError& err);
+
+                /**
+                 * Called if notification handling failed.
+                 *
+                 * @param id Channel ID.
+                 * @param err Error.
+                 */
+                virtual void OnNotificationHandlingError(uint64_t id, const IgniteError& err);
 
                 /**
                  * Synchronously send request message and receive response.
@@ -118,19 +164,7 @@
                  * @return Channel that was used for request.
                  * @throw IgniteError on error.
                  */
-                template<typename ReqT, typename RspT>
-                SP_DataChannel SyncMessage(const ReqT& req, RspT& rsp)
-                {
-                    SP_DataChannel channel = GetRandomChannel();
-
-                    int32_t metaVer = typeMgr.GetVersion();
-
-                    SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
-
-                    ProcessMeta(metaVer);
-
-                    return channel;
-                }
+                SP_DataChannel SyncMessage(Request& req, Response& rsp);
 
                 /**
                  * Synchronously send request message and receive response.
@@ -141,19 +175,7 @@
                  * @return Channel that was used for request.
                  * @throw IgniteError on error.
                  */
-                template<typename ReqT, typename RspT>
-                SP_DataChannel SyncMessage(const ReqT& req, RspT& rsp, const Guid& hint)
-                {
-                    SP_DataChannel channel = GetBestChannel(hint);
-
-                    int32_t metaVer = typeMgr.GetVersion();
-
-                    SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
-
-                    ProcessMeta(metaVer);
-
-                    return channel;
-                }
+                SP_DataChannel SyncMessage(Request& req, Response& rsp, const Guid& hint);
 
                 /**
                  * Synchronously send request message and receive response.
@@ -165,55 +187,7 @@
                  * @return Channel that was used for request.
                  * @throw IgniteError on error.
                  */
-                template<typename ReqT, typename RspT>
-                SP_DataChannel SyncMessageNoMetaUpdate(const ReqT& req, RspT& rsp)
-                {
-                    SP_DataChannel channel = GetRandomChannel();
-
-                    SyncMessagePreferredChannelNoMetaUpdate(req, rsp, channel);
-
-                    return channel;
-                }
-
-                /**
-                 * Synchronously send request message, receive response and get a notification.
-                 *
-                 * @param req Request message.
-                 * @param notification Notification message.
-                 * @return Channel that was used for request.
-                 * @throw IgniteError on error.
-                 */
-                template<typename ReqT, typename NotT>
-                SP_DataChannel SyncMessageWithNotification(const ReqT& req, NotT& notification)
-                {
-                    SP_DataChannel channel = GetRandomChannel();
-
-                    if (!channel.IsValid())
-                    {
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                                          "Can not connect to any available cluster node. Please restart client");
-                    }
-
-                    int32_t metaVer = typeMgr.GetVersion();
-
-                    try
-                    {
-                        channel.Get()->SyncMessageWithNotification(req, notification, ioTimeout);
-                    }
-                    catch (IgniteError& err)
-                    {
-                        InvalidateChannel(channel);
-
-                        std::string msg("Connection failure during command processing. Please re-run command. Cause: ");
-                        msg += err.GetText();
-
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
-                    }
-
-                    ProcessMeta(metaVer);
-
-                    return channel;
-                }
+                SP_DataChannel SyncMessageNoMetaUpdate(Request& req, Response& rsp);
 
                 /**
                  * Update affinity mapping for the cache.
@@ -252,15 +226,22 @@
                  *
                  * @return IO timeout.
                  */
-                int32_t GetIoTimeout()
+                int32_t GetIoTimeout() const
                 {
-                    return ioTimeout;
+                    return config.GetConnectionTimeout();
                 }
 
             private:
                 IGNITE_NO_COPY_ASSIGNMENT(DataRouter);
 
                 /**
+                 * Make sure that there is at least one connection to a cluster. Wait for specified timeout.
+                 * @param timeout Timeout.
+                 * @return @c true if connected, @c false otherwise.
+                 */
+                bool EnsureConnected(int32_t timeout);
+
+                /**
                  * Invalidate provided data channel.
                  *
                  * @param channel Data channel.
@@ -268,46 +249,39 @@
                 void InvalidateChannel(SP_DataChannel& channel);
 
                 /**
+                 * Invalidate provided data channel.
+                 *
+                 * @warning Should be only called with locked channelsMutex.
+                 * @param channel Data channel.
+                 */
+                void InvalidateChannelLocked(SP_DataChannel& channel);
+
+                /**
+                 * Process meta if needed.
+                 *
+                 * @param metaVer Version of meta.
+                 */
+                void ProcessMeta(int32_t metaVer);
+
+                /**
+                 * Update affinity if needed.
+                 *
+                 * @param rsp Response.
+                 */
+                void CheckAffinity(Response& rsp);
+
+                /**
                  * Synchronously send request message and receive response.
                  *
                  * @param req Request message.
                  * @param rsp Response message.
                  * @param preferred Preferred channel to use.
                  * @throw IgniteError on error.
+                 *
+                 * @return Data channel that was used.
                  */
-                template<typename ReqT, typename RspT>
-                void SyncMessagePreferredChannelNoMetaUpdate(const ReqT& req, RspT& rsp, const SP_DataChannel& preferred)
-                {
-                    SP_DataChannel channel = preferred;
-
-                    if (!channel.IsValid())
-                        channel = GetRandomChannel();
-
-                    if (!channel.IsValid())
-                    {
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE,
-                            "Can not connect to any available cluster node. Please restart client");
-                    }
-
-                    try
-                    {
-                        channel.Get()->SyncMessage(req, rsp, ioTimeout);
-                    }
-                    catch (IgniteError& err)
-                    {
-                        InvalidateChannel(channel);
-
-                        std::string msg("Connection failure during command processing. Please re-run command. Cause: ");
-                        msg += err.GetText();
-
-                        throw IgniteError(IgniteError::IGNITE_ERR_NETWORK_FAILURE, msg.c_str());
-                    }
-
-                    CheckAffinity(rsp);
-                }
-
-                /** Shared pointer to end points. */
-                typedef common::concurrent::SharedPointer<network::EndPoints> SP_EndPoints;
+                SP_DataChannel SyncMessagePreferredChannelNoMetaUpdate(Request& req, Response& rsp,
+                    const SP_DataChannel& preferred);
 
                 /**
                  * Get random data channel.
@@ -322,15 +296,7 @@
                  *
                  * @return Random data channel or null, if not connected.
                  */
-                SP_DataChannel GetRandomChannelUnsafe();
-
-                /**
-                 * Check whether the provided end point is provided by user using configuration.
-                 *
-                 * @param endPoint End point to check.
-                 * @return @c true if provided by user using configuration.
-                 */
-                bool IsProvidedByUser(const network::EndPoint& endPoint);
+                SP_DataChannel GetRandomChannelLocked();
 
                 /**
                  * Get the best data channel.
@@ -348,11 +314,30 @@
                  */
                 static void CollectAddresses(const std::string& str, std::vector<network::TcpRange>& ranges);
 
-                /** IO timeout in seconds. */
-                int32_t ioTimeout;
+                /**
+                 * Check whether there were any critical errors during handshake.
+                 * @warning May only be called when lock is held!
+                 *
+                 * @throw IgniteError if there is error.
+                 */
+                void CheckHandshakeErrorLocked();
 
-                /** Connection timeout in seconds. */
-                int32_t connectionTimeout;
+                /**
+                 * Find channel by ID.
+                 *
+                 * @param id Channel ID
+                 * @return Channel or null if is not present.
+                 */
+                SP_DataChannel FindChannel(uint64_t id);
+
+                /**
+                 * Find channel by ID.
+                 * @warning May only be called when lock is held!
+                 *
+                 * @param id Channel ID
+                 * @return Channel or null if is not present.
+                 */
+                SP_DataChannel FindChannelLocked(uint64_t id);
 
                 /** Configuration. */
                 ignite::thin::IgniteClientConfiguration config;
@@ -360,23 +345,38 @@
                 /** Address ranges. */
                 std::vector<network::TcpRange> ranges;
 
+                /** Async client pool */
+                network::SP_AsyncClientPool asyncPool;
+
                 /** Type updater. */
                 std::auto_ptr<binary::BinaryTypeUpdater> typeUpdater;
 
                 /** Metadata manager. */
                 binary::BinaryTypeManager typeMgr;
 
-                /** Data channels. */
-                ChannelsGuidMap channels;
+                /** All data channels. */
+                ChannelsIdMap channels;
 
-                /** Data channels. */
-                ChannelsVector legacyChannels;
+                /** Partition awareness data channels. */
+                ChannelsGuidMap partChannels;
+
+                /** Channel that complete handshake successfully. */
+                ChannelsIdSet connectedChannels;
 
                 /** Channels mutex. */
                 common::concurrent::CriticalSection channelsMutex;
 
+                /** Channels connection wait point. */
+                common::concurrent::ConditionVariable channelsWaitPoint;
+
+                /** Last handshake error. */
+                std::auto_ptr<IgniteError> lastHandshakeError;
+
                 /** Cache affinity manager. */
                 affinity::AffinityManager affinityManager;
+
+                /** Thread pool to dispatch user code execution. */
+                common::ThreadPool userThreadPool;
             };
 
             /** Shared pointer type. */
diff --git a/modules/platforms/cpp/thin-client/src/impl/ignite_client_impl.cpp b/modules/platforms/cpp/thin-client/src/impl/ignite_client_impl.cpp
index 92eec00..f153579 100644
--- a/modules/platforms/cpp/thin-client/src/impl/ignite_client_impl.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/ignite_client_impl.cpp
@@ -41,7 +41,9 @@
 
             IgniteClientImpl::~IgniteClientImpl()
             {
-                // No-op.
+                DataRouter* router0 = router.Get();
+                if (router0)
+                    router0->Close();
             }
 
             void IgniteClientImpl::Start()
@@ -109,7 +111,7 @@
 
             void IgniteClientImpl::GetCacheNames(std::vector<std::string>& cacheNames)
             {
-                Request<RequestType::CACHE_GET_NAMES> req;
+                RequestAdapter<MessageType::CACHE_GET_NAMES> req;
                 GetCacheNamesResponse rsp(cacheNames);
 
                 router.Get()->SyncMessage(req, rsp);
diff --git a/modules/platforms/cpp/thin-client/src/impl/message.cpp b/modules/platforms/cpp/thin-client/src/impl/message.cpp
index 5bf5329..8390e2d 100644
--- a/modules/platforms/cpp/thin-client/src/impl/message.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/message.cpp
@@ -20,6 +20,7 @@
 
 #include <ignite/impl/thin/writable.h>
 #include <ignite/impl/thin/readable.h>
+#include <ignite/impl/thin/cache/continuous/continuous_query_client_holder.h>
 
 #include "impl/response_status.h"
 #include "impl/data_channel.h"
@@ -31,30 +32,17 @@
     {
         namespace thin
         {
-            /**
-             * Message flags.
-             */
-            struct Flag
-            {
-                enum Type
-                {
-                    /** Failure flag. */
-                    FAILURE = 1,
-
-                    /** Affinity topology change flag. */
-                    AFFINITY_TOPOLOGY_CHANGED = 1 << 1,
-
-                    /** Server notification flag. */
-                    NOTIFICATION = 1 << 2
-                };
-            };
-
             CachePartitionsRequest::CachePartitionsRequest(const std::vector<int32_t>& cacheIds) :
                 cacheIds(cacheIds)
             {
                 // No-op.
             }
 
+            void ResourceCloseRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion&) const
+            {
+                writer.WriteInt64(id);
+            }
+
             void CachePartitionsRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion&) const
             {
                 writer.WriteInt32(static_cast<int32_t>(cacheIds.size()));
@@ -134,30 +122,6 @@
                 return (flags & Flag::FAILURE) != 0;
             }
 
-            ClientCacheNodePartitionsResponse::ClientCacheNodePartitionsResponse(
-                std::vector<NodePartitions>& nodeParts):
-                nodeParts(nodeParts)
-            {
-                // No-op.
-            }
-
-            ClientCacheNodePartitionsResponse::~ClientCacheNodePartitionsResponse()
-            {
-                // No-op.
-            }
-
-            void ClientCacheNodePartitionsResponse::ReadOnSuccess(
-                binary::BinaryReaderImpl& reader, const ProtocolVersion&)
-            {
-                int32_t num = reader.ReadInt32();
-
-                nodeParts.clear();
-                nodeParts.resize(static_cast<size_t>(num));
-
-                for (int32_t i = 0; i < num; ++i)
-                    nodeParts[i].Read(reader);
-            }
-
             CachePartitionsResponse::CachePartitionsResponse(std::vector<PartitionAwarenessGroup>& groups) :
                 groups(groups)
             {
@@ -292,7 +256,7 @@
             }
 
             CacheGetSizeRequest::CacheGetSizeRequest(int32_t cacheId, bool binary, int32_t peekModes) :
-                CacheRequest<RequestType::CACHE_GET_SIZE>(cacheId, binary),
+                CacheRequest<MessageType::CACHE_GET_SIZE>(cacheId, binary),
                 peekModes(peekModes)
             {
                 // No-op.
@@ -300,7 +264,7 @@
 
             void CacheGetSizeRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion& ver) const
             {
-                CacheRequest<RequestType::CACHE_GET_SIZE>::Write(writer, ver);
+                CacheRequest<MessageType::CACHE_GET_SIZE>::Write(writer, ver);
 
                 if (peekModes & ignite::thin::cache::CachePeekMode::ALL)
                 {
@@ -353,7 +317,7 @@
                 int32_t cacheId,
                 const ignite::thin::cache::query::SqlFieldsQuery &qry
                 ) :
-                CacheRequest<RequestType::QUERY_SQL_FIELDS>(cacheId, false),
+                CacheRequest<MessageType::QUERY_SQL_FIELDS>(cacheId, false),
                 qry(qry)
             {
                 // No-op.
@@ -361,7 +325,7 @@
 
             void SqlFieldsQueryRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion& ver) const
             {
-                CacheRequest<RequestType::QUERY_SQL_FIELDS>::Write(writer, ver);
+                CacheRequest<MessageType::QUERY_SQL_FIELDS>::Write(writer, ver);
 
                 if (qry.schema.empty())
                     writer.WriteNull();
@@ -413,11 +377,28 @@
                 writer.WriteInt64(cursorId);
             }
 
-            void SqlFieldsCursorGetPageResponse::ReadOnSuccess(binary::BinaryReaderImpl&reader, const ProtocolVersion&)
+            void SqlFieldsCursorGetPageResponse::ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&)
             {
                 cursorPage.Get()->Read(reader);
             }
 
+            void ContinuousQueryRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion& ver) const
+            {
+                CacheRequest<MessageType::QUERY_CONTINUOUS>::Write(writer, ver);
+
+                writer.WriteInt32(pageSize);
+                writer.WriteInt64(timeInterval);
+                writer.WriteBool(includeExpired);
+
+                // TODO: IGNITE-16291: Implement remote filters for Continuous Queries.
+                writer.WriteNull();
+            }
+
+            void ContinuousQueryResponse::ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&)
+            {
+                queryId = reader.ReadInt64();
+            }
+
             void ComputeTaskExecuteRequest::Write(binary::BinaryWriterImpl& writer, const ProtocolVersion&) const
             {
                 // To be changed when Cluster API is implemented.
@@ -435,31 +416,15 @@
                 taskId = reader.ReadInt64();
             }
 
-            void ComputeTaskFinishedNotification::Read(binary::BinaryReaderImpl& reader, const ProtocolVersion&)
+            void ComputeTaskFinishedNotification::ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&)
             {
-                int16_t flags = reader.ReadInt16();
-                if (!(flags & Flag::NOTIFICATION))
-                {
-                    IGNITE_ERROR_FORMATTED_1(IgniteError::IGNITE_ERR_GENERIC, "Was expecting notification but got "
-                        "different kind of message", "flags", flags)
-                }
+                result.Read(reader);
+            }
 
-                int16_t opCode = reader.ReadInt16();
-                if (opCode != RequestType::COMPUTE_TASK_FINISHED)
-                {
-                    IGNITE_ERROR_FORMATTED_2(IgniteError::IGNITE_ERR_GENERIC, "Unexpected notification type",
-                        "expected", (int)RequestType::COMPUTE_TASK_FINISHED, "actual", opCode)
-                }
-
-                if (flags & Flag::FAILURE)
-                {
-                    status = reader.ReadInt32();
-                    reader.ReadString(errorMessage);
-                }
-                else
-                {
-                    result.Read(reader);
-                }
+            void ClientCacheEntryEventNotification::ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&)
+            {
+                ignite::binary::BinaryRawReader reader0(&reader);
+                query.ReadAndProcessEvents(reader0);
             }
         }
     }
diff --git a/modules/platforms/cpp/thin-client/src/impl/message.h b/modules/platforms/cpp/thin-client/src/impl/message.h
index 88e7f29..dad00c7 100644
--- a/modules/platforms/cpp/thin-client/src/impl/message.h
+++ b/modules/platforms/cpp/thin-client/src/impl/message.h
@@ -53,6 +53,18 @@
             /* Forward declaration. */
             class Writable;
 
+            namespace cache
+            {
+                namespace query
+                {
+                    namespace continuous
+                    {
+                        /* Forward declaration. */
+                        class ContinuousQueryClientHolderBase;
+                    }
+                }
+            }
+
             struct ClientType
             {
                 enum Type
@@ -61,7 +73,7 @@
                 };
             };
 
-            struct RequestType
+            struct MessageType
             {
                 enum Type
                 {
@@ -158,6 +170,12 @@
                     /** SQL fields query get next cursor page request. */
                     QUERY_SQL_FIELDS_CURSOR_GET_PAGE = 2005,
 
+                    /** Continuous query. */
+                    QUERY_CONTINUOUS = 2006,
+
+                    /** Continuous query notification event. */
+                    QUERY_CONTINUOUS_EVENT_NOTIFICATION = 2007,
+
                     /** Get binary type info. */
                     GET_BINARY_TYPE = 3002,
 
@@ -179,15 +197,39 @@
             };
 
             /**
-             * Request.
-             *
-             * @tparam OpCode Operation code.
+             * Message flags.
              */
-            template<int32_t OpCode>
+            struct Flag
+            {
+                enum Type
+                {
+                    /** Failure flag. */
+                    FAILURE = 1,
+
+                    /** Affinity topology change flag. */
+                    AFFINITY_TOPOLOGY_CHANGED = 1 << 1,
+
+                    /** Server notification flag. */
+                    NOTIFICATION = 1 << 2
+                };
+            };
+
+            /**
+             * Request.
+             */
             class Request
             {
             public:
                 /**
+                 * Constructor.
+                 */
+                Request() :
+                    id(0)
+                {
+                    // No-op.
+                }
+
+                /**
                  * Destructor.
                  */
                 virtual ~Request()
@@ -200,10 +242,7 @@
                  *
                  * @return Operation code.
                  */
-                static int32_t GetOperationCode()
-                {
-                    return OpCode;
-                }
+                virtual int16_t GetOperationCode() const = 0;
 
                 /**
                  * Write request using provided writer.
@@ -214,12 +253,101 @@
                 {
                     // No-op.
                 }
+
+                /**
+                 * Set request ID.
+                 *
+                 * @param id ID.
+                 */
+                void SetId(int64_t id)
+                {
+                    this->id = id;
+                }
+
+                /**
+                 * Get request ID.
+                 *
+                 * @return ID.
+                 */
+                int64_t GetId() const
+                {
+                    return id;
+                }
+
+            private:
+                /** Request ID. Only set when request is sent. */
+                int64_t id;
+            };
+
+            /**
+             * Request adapter.
+             *
+             * @tparam OpCode Operation code.
+             */
+            template<int16_t OpCode>
+            class RequestAdapter : public Request
+            {
+            public:
+                /**
+                 * Destructor.
+                 */
+                virtual ~RequestAdapter()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Get operation code.
+                 *
+                 * @return Operation code.
+                 */
+                virtual int16_t GetOperationCode() const
+                {
+                    return OpCode;
+                }
             };
 
             /**
              * Cache partitions request.
              */
-            class CachePartitionsRequest : public Request<RequestType::CACHE_PARTITIONS>
+            class ResourceCloseRequest : public RequestAdapter<MessageType::RESOURCE_CLOSE>
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param id Resource ID.
+                 */
+                ResourceCloseRequest(int64_t id) :
+                    id(id)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~ResourceCloseRequest()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Write request using provided writer.
+                 *
+                 * @param writer Writer.
+                 */
+                virtual void Write(binary::BinaryWriterImpl& writer, const ProtocolVersion&) const;
+
+            private:
+                /** Resource ID. */
+                const int64_t id;
+            };
+
+            /**
+             * Cache partitions request.
+             */
+            class CachePartitionsRequest : public RequestAdapter<MessageType::CACHE_PARTITIONS>
             {
             public:
                 /**
@@ -251,7 +379,7 @@
             /**
              * Get or create cache request.
              */
-            class GetOrCreateCacheWithNameRequest : public Request<RequestType::CACHE_GET_OR_CREATE_WITH_NAME>
+            class GetOrCreateCacheWithNameRequest : public RequestAdapter<MessageType::CACHE_GET_OR_CREATE_WITH_NAME>
             {
             public:
                 /**
@@ -284,7 +412,7 @@
             /**
              * Get or create cache request.
              */
-            class CreateCacheWithNameRequest : public Request<RequestType::CACHE_CREATE_WITH_NAME>
+            class CreateCacheWithNameRequest : public RequestAdapter<MessageType::CACHE_CREATE_WITH_NAME>
             {
             public:
                 /**
@@ -317,7 +445,7 @@
             /**
              * Destroy cache request.
              */
-            class DestroyCacheRequest : public Request<RequestType::CACHE_DESTROY>
+            class DestroyCacheRequest : public RequestAdapter<MessageType::CACHE_DESTROY>
             {
             public:
                 /**
@@ -357,7 +485,7 @@
              * Request to cache.
              */
             template<int32_t OpCode>
-            class CacheRequest : public Request<OpCode>
+            class CacheRequest : public RequestAdapter<OpCode>
             {
             public:
                 /**
@@ -431,7 +559,7 @@
             /**
              * Cache get size request.
              */
-            class CacheGetSizeRequest : public CacheRequest<RequestType::CACHE_GET_SIZE>
+            class CacheGetSizeRequest : public CacheRequest<MessageType::CACHE_GET_SIZE>
             {
             public:
                 /**
@@ -625,7 +753,7 @@
             /**
              * Tx start request.
              */
-            class TxStartRequest : public Request<RequestType::OP_TX_START>
+            class TxStartRequest : public RequestAdapter<MessageType::OP_TX_START>
             {
             public:
                 /**
@@ -682,7 +810,7 @@
             /**
              * Tx end request.
              */
-            class TxEndRequest : public Request<RequestType::OP_TX_END>
+            class TxEndRequest : public RequestAdapter<MessageType::OP_TX_END>
             {
             public:
                 /**
@@ -727,7 +855,7 @@
             /**
              * Cache get binary type request.
              */
-            class BinaryTypeGetRequest : public Request<RequestType::GET_BINARY_TYPE>
+            class BinaryTypeGetRequest : public RequestAdapter<MessageType::GET_BINARY_TYPE>
             {
             public:
                 /**
@@ -764,7 +892,7 @@
             /**
              * Cache put binary type request.
              */
-            class BinaryTypePutRequest : public Request<RequestType::PUT_BINARY_TYPE>
+            class BinaryTypePutRequest : public RequestAdapter<MessageType::PUT_BINARY_TYPE>
             {
             public:
                 /**
@@ -801,7 +929,7 @@
             /**
              * Cache SQL fields query request.
              */
-            class SqlFieldsQueryRequest : public CacheRequest<RequestType::QUERY_SQL_FIELDS>
+            class SqlFieldsQueryRequest : public CacheRequest<MessageType::QUERY_SQL_FIELDS>
             {
             public:
                 /**
@@ -835,7 +963,7 @@
             /**
              * Cache SQL fields cursor get page request.
              */
-            class SqlFieldsCursorGetPageRequest : public Request<RequestType::QUERY_SQL_FIELDS_CURSOR_GET_PAGE>
+            class SqlFieldsCursorGetPageRequest : public RequestAdapter<MessageType::QUERY_SQL_FIELDS_CURSOR_GET_PAGE>
             {
             public:
                 /**
@@ -870,9 +998,58 @@
             };
 
             /**
+             * Continuous query request.
+             */
+            class ContinuousQueryRequest : public CacheRequest<MessageType::QUERY_CONTINUOUS>
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param cacheId Cache ID.
+                 * @param pageSize Page size.
+                 * @param timeInterval Time interval.
+                 * @param includeExpired Include expired.
+                 */
+                explicit ContinuousQueryRequest(int32_t cacheId, int32_t pageSize, int64_t timeInterval, bool includeExpired) :
+                    CacheRequest(cacheId, false),
+                    pageSize(pageSize),
+                    timeInterval(timeInterval),
+                    includeExpired(includeExpired)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~ContinuousQueryRequest()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Write request using provided writer.
+                 * @param writer Writer.
+                 * @param ver Version.
+                 */
+                virtual void Write(binary::BinaryWriterImpl& writer, const ProtocolVersion& ver) const;
+
+            private:
+                /** Page size. */
+                const int32_t pageSize;
+
+                /** Time interval. */
+                const int64_t timeInterval;
+
+                /** Include expired. */
+                const bool includeExpired;
+            };
+
+            /**
              * Compute task execute request.
              */
-            class ComputeTaskExecuteRequest : public Request<RequestType::COMPUTE_TASK_EXECUTE>
+            class ComputeTaskExecuteRequest : public RequestAdapter<MessageType::COMPUTE_TASK_EXECUTE>
             {
             public:
                 /**
@@ -943,7 +1120,7 @@
                  * @param reader Reader.
                  * @param ver Protocol version.
                  */
-                void Read(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver);
+                virtual void Read(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver);
 
                 /**
                  * Get request processing status.
@@ -999,7 +1176,6 @@
                     // No-op.
                 }
 
-            private:
                 /** Flags. */
                 int16_t flags;
 
@@ -1016,36 +1192,6 @@
             /**
              * Cache node list request.
              */
-            class ClientCacheNodePartitionsResponse : public Response
-            {
-            public:
-                /**
-                 * Constructor.
-                 *
-                 * @param nodeParts Node partitions.
-                 */
-                ClientCacheNodePartitionsResponse(std::vector<NodePartitions>& nodeParts);
-
-                /**
-                 * Destructor.
-                 */
-                virtual ~ClientCacheNodePartitionsResponse();
-
-                /**
-                 * Read data if response status is ResponseStatus::SUCCESS.
-                 *
-                 * @param reader Reader.
-                 */
-                virtual void ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&);
-
-            private:
-                /** Node partitions. */
-                std::vector<NodePartitions>& nodeParts;
-            };
-
-            /**
-             * Cache node list request.
-             */
             class CachePartitionsResponse : public Response
             {
             public:
@@ -1451,6 +1597,50 @@
             };
 
             /**
+             * Cache Continuous Query response.
+             */
+            class ContinuousQueryResponse : public Response
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                ContinuousQueryResponse() :
+                    queryId(0)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~ContinuousQueryResponse()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Get cursor page.
+                 * @return Cursor page.
+                 */
+                int64_t GetQueryId() const
+                {
+                    return queryId;
+                }
+
+                /**
+                 * Read data if response status is ResponseStatus::SUCCESS.
+                 *
+                 * @param reader Reader.
+                 */
+                virtual void ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion&);
+
+            private:
+                /** Query ID. */
+                int64_t queryId;
+            };
+
+            /**
              * Compute task execute response.
              */
             class ComputeTaskExecuteResponse : public Response
@@ -1497,17 +1687,107 @@
             /**
              * Compute task finished notification.
              */
-            class ComputeTaskFinishedNotification
+            class Notification : public Response
             {
             public:
-                typedef ComputeTaskExecuteResponse ResponseType;
-
                 /**
                  * Constructor.
                  */
-                ComputeTaskFinishedNotification(Readable& result) :
-                    status(0),
-                    errorMessage(),
+                Notification()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~Notification()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Read notification data.
+                 *
+                 * @param reader Reader.
+                 */
+                virtual void Read(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver)
+                {
+                    flags = reader.ReadInt16();
+
+                    int16_t readOpCode = reader.ReadInt16();
+                    if (readOpCode != GetOperationCode())
+                    {
+                        IGNITE_ERROR_FORMATTED_2(IgniteError::IGNITE_ERR_GENERIC, "Unexpected notification type",
+                             "expected", GetOperationCode(), "actual", readOpCode)
+                    }
+
+                    if (IsFailure())
+                    {
+                        status = reader.ReadInt32();
+                        reader.ReadString(error);
+
+                        return;
+                    }
+
+                    ReadOnSuccess(reader, ver);
+                }
+
+                /**
+                 * Get operation code.
+                 *
+                 * @return Operation code.
+                 */
+                virtual int16_t GetOperationCode() const = 0;
+
+            protected:
+                /**
+                 * Read data if response status is ResponseStatus::SUCCESS.
+                 *
+                 * @param reader Reader.
+                 * @param ver Version.
+                 */
+                virtual void ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver) = 0;
+            };
+
+            /**
+             * Request adapter.
+             *
+             * @tparam OpCode Operation code.
+             */
+            template<int16_t OpCode>
+            class NotificationAdapter : public Notification
+            {
+            public:
+                /**
+                 * Destructor.
+                 */
+                virtual ~NotificationAdapter()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Get operation code.
+                 *
+                 * @return Operation code.
+                 */
+                virtual int16_t GetOperationCode() const
+                {
+                    return OpCode;
+                }
+            };
+
+            /**
+             * Compute task finished notification.
+             */
+            class ComputeTaskFinishedNotification : public NotificationAdapter<MessageType::COMPUTE_TASK_FINISHED>
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                explicit ComputeTaskFinishedNotification(Readable& result) :
                     result(result)
                 {
                     // No-op.
@@ -1522,40 +1802,53 @@
                 }
 
                 /**
-                 * Check if the message is failure.
-                 * @return @c true on failure.
-                 */
-                bool IsFailure() const
-                {
-                    return !errorMessage.empty();
-                }
-
-                /**
-                 * Get error message.
-                 * @return Error message.
-                 */
-                const std::string& GetErrorMessage() const
-                {
-                    return errorMessage;
-                }
-
-                /**
-                 * Read response using provided reader.
+                 * Read data if response status is ResponseStatus::SUCCESS.
+                 *
                  * @param reader Reader.
-                 * @param ver Protocol version.
+                 * @param ver Version.
                  */
-                void Read(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver);
+                virtual void ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver);
 
             private:
-                /** Status. */
-                int32_t status;
-
-                /** Error message. */
-                std::string errorMessage;
-
                 /** Result. */
                 Readable& result;
             };
+
+            /**
+             * Continuous query notification.
+             */
+            class ClientCacheEntryEventNotification : public NotificationAdapter<MessageType::QUERY_CONTINUOUS_EVENT_NOTIFICATION>
+            {
+            public:
+                /**
+                 * Constructor.
+                 */
+                explicit ClientCacheEntryEventNotification(cache::query::continuous::ContinuousQueryClientHolderBase& query) :
+                    query(query)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~ClientCacheEntryEventNotification()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Read data if response status is ResponseStatus::SUCCESS.
+                 *
+                 * @param reader Reader.
+                 * @param ver Version.
+                 */
+                virtual void ReadOnSuccess(binary::BinaryReaderImpl& reader, const ProtocolVersion& ver);
+
+            private:
+                /** Result. */
+                cache::query::continuous::ContinuousQueryClientHolderBase& query;
+            };
         }
     }
 }
diff --git a/modules/platforms/cpp/thin-client/src/impl/notification_handler.h b/modules/platforms/cpp/thin-client/src/impl/notification_handler.h
new file mode 100644
index 0000000..de9e88c
--- /dev/null
+++ b/modules/platforms/cpp/thin-client/src/impl/notification_handler.h
@@ -0,0 +1,284 @@
+/*
+ * 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.
+ */
+
+#ifndef _IGNITE_IMPL_THIN_NOTIFICATION_HANDLER
+#define _IGNITE_IMPL_THIN_NOTIFICATION_HANDLER
+
+#include <stdint.h>
+
+#include <vector>
+
+#include <ignite/ignite_error.h>
+#include <ignite/common/thread_pool.h>
+#include <ignite/network/data_buffer.h>
+
+#include <ignite/impl/interop/interop_memory.h>
+
+namespace ignite
+{
+    namespace impl
+    {
+        namespace thin
+        {
+            /** Notification handler. */
+            class NotificationHandler
+            {
+            public:
+                /**
+                 * Destructor.
+                 */
+                virtual ~NotificationHandler()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Handle notification.
+                 *
+                 * @param msg Message.
+                 * @return @c true if processing complete.
+                 */
+                virtual void OnNotification(const network::DataBuffer& msg) = 0;
+
+                /**
+                 * Disconnected callback.
+                 *
+                 * Called if channel was disconnected.
+                 */
+                virtual void OnDisconnected() = 0;
+            };
+
+            /** Shared pointer to notification handler. */
+            typedef common::concurrent::SharedPointer<NotificationHandler> SP_NotificationHandler;
+
+            /**
+             * Task that handles notification
+             */
+            class HandleNotificationTask : public common::ThreadPoolTask
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param msg Message.
+                 * @param handler Notification handler.
+                 * @param channelId Channel ID.
+                 * @param channelStateHandler Channel state handler.
+                 */
+                HandleNotificationTask(
+                    const network::DataBuffer& msg,
+                    const SP_NotificationHandler& handler,
+                    uint64_t channelId,
+                    ChannelStateHandler& channelStateHandler
+                ) :
+                    msg(msg),
+                    handler(handler),
+                    channelId(channelId),
+                    channelStateHandler(channelStateHandler)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~HandleNotificationTask()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Execute task.
+                 */
+                virtual void Execute()
+                {
+                    handler.Get()->OnNotification(msg);
+                }
+
+                /**
+                 * Called if error occurred during task processing.
+                 *
+                 * @param err Error.
+                 */
+                virtual void OnError(const IgniteError& err)
+                {
+                    channelStateHandler.OnNotificationHandlingError(channelId, err);
+                }
+
+            private:
+                /** Message. */
+                network::DataBuffer msg;
+
+                /** Handler. */
+                SP_NotificationHandler handler;
+
+                /** Channel ID. */
+                uint64_t channelId;
+
+                /** Channel state handler. */
+                ChannelStateHandler& channelStateHandler;
+            };
+
+            /**
+             * Task that handles connection closing
+             */
+            class DisconnectedTask : public common::ThreadPoolTask
+            {
+            public:
+                /**
+                 * Constructor.
+                 *
+                 * @param handler Notification handler.
+                 */
+                explicit DisconnectedTask(const SP_NotificationHandler& handler) :
+                    handler(handler)
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                virtual ~DisconnectedTask()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Execute task.
+                 */
+                virtual void Execute()
+                {
+                    handler.Get()->OnDisconnected();
+                }
+
+                /**
+                 * Called if error occurred during task processing.
+                 *
+                 * @param err Error.
+                 */
+                virtual void OnError(const IgniteError&)
+                {
+                    // No-op. Connection already closed so there is not much we can do.
+                    // TODO: Add logging here once it's implemented.
+                }
+
+            private:
+                /** Handler. */
+                SP_NotificationHandler handler;
+            };
+
+
+            /** Notification handler. */
+            class NotificationHandlerHolder
+            {
+                /** Message queue. */
+                typedef std::vector<network::DataBuffer> MessageQueue;
+
+            public:
+                /**
+                 * Default constructor.
+                 */
+                NotificationHandlerHolder() :
+                    disconnected(false),
+                    queue(),
+                    handler()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Destructor.
+                 */
+                ~NotificationHandlerHolder()
+                {
+                    // No-op.
+                }
+
+                /**
+                 * Process notification.
+                 *
+                 * @param msg Notification message to process.
+                 * @param channelId Channel ID.
+                 * @param channelStateHandler Channel state handler.
+                 * @return Task for dispatching if handler is present and null otherwise.
+                 */
+                common::SP_ThreadPoolTask ProcessNotification(const network::DataBuffer& msg,
+                    uint64_t channelId, ChannelStateHandler& channelStateHandler)
+                {
+                    network::DataBuffer notification(msg.Clone());
+
+                    if (handler.IsValid())
+                        return common::SP_ThreadPoolTask(
+                            new HandleNotificationTask(notification, handler, channelId, channelStateHandler));
+
+                    queue.push_back(notification);
+
+                    return common::SP_ThreadPoolTask();
+                }
+
+                /**
+                 * Process disconnect.
+                 *
+                 * @return Task for dispatching if handler is present and null otherwise.
+                 */
+                common::SP_ThreadPoolTask ProcessClosed()
+                {
+                    disconnected = true;
+
+                    if (handler.IsValid())
+                        return common::SP_ThreadPoolTask(new DisconnectedTask(handler));
+
+                    return common::SP_ThreadPoolTask();
+                }
+
+                /**
+                 * Set handler.
+                 *
+                 * @param handler Notification handler.
+                 */
+                void SetHandler(const SP_NotificationHandler& handler0)
+                {
+                    if (handler.IsValid())
+                        throw IgniteError(IgniteError::IGNITE_ERR_GENERIC,
+                            "Internal error: handler is already set for the notification");
+
+                    handler = handler0;
+                    for (MessageQueue::iterator it = queue.begin(); it != queue.end(); ++it)
+                        handler.Get()->OnNotification(*it);
+
+                    queue.clear();
+
+                    if (disconnected)
+                        handler.Get()->OnDisconnected();
+                }
+
+            private:
+                /** Disconnected flag. */
+                bool disconnected;
+
+                /** Notification queue. */
+                MessageQueue queue;
+
+                /** Notification handler. */
+                SP_NotificationHandler handler;
+            };
+        }
+    }
+}
+
+#endif //_IGNITE_IMPL_THIN_NOTIFICATION_HANDLER
diff --git a/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.cpp b/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.cpp
index 6fd9115..391096c 100644
--- a/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.cpp
@@ -33,7 +33,7 @@
             namespace transactions
             {
                 template<typename ReqT, typename RspT>
-                void TransactionImpl::SendTxMessage(const ReqT& req, RspT& rsp)
+                void TransactionImpl::SendTxMessage(ReqT& req, RspT& rsp)
                 {
                     channel.Get()->SyncMessage(req, rsp, static_cast<int32_t>(timeout / 1000) + ioTimeout);
 
diff --git a/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.h b/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.h
index e395b3b..8e3cfc9 100644
--- a/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.h
+++ b/modules/platforms/cpp/thin-client/src/impl/transactions/transaction_impl.h
@@ -170,7 +170,7 @@
                      * @throw IgniteError on error.
                      */
                     template<typename ReqT, typename RspT>
-                    void SendTxMessage(const ReqT& req, RspT& rsp);
+                    void SendTxMessage(ReqT& req, RspT& rsp);
 
                     /** Data channel to use. */
                     SP_DataChannel channel;
diff --git a/modules/platforms/cpp/thin-client/src/impl/utility.cpp b/modules/platforms/cpp/thin-client/src/impl/utility.cpp
index c0046b6..9f6850e 100644
--- a/modules/platforms/cpp/thin-client/src/impl/utility.cpp
+++ b/modules/platforms/cpp/thin-client/src/impl/utility.cpp
@@ -159,6 +159,7 @@
                     return static_cast<uint16_t>(intPort);
                 }
 
+                IGNORE_SIGNED_OVERFLOW
                 int32_t GetCacheId(const char* cacheName)
                 {
                     if (!cacheName)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/EnumsTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/EnumsTest.cs
index 4064151..e2de729 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/EnumsTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/EnumsTest.cs
@@ -307,7 +307,82 @@
 
             CheckSerializeDeserialize(val);
         }
+        
+        /// <summary>
+        /// Tests enums serialization by single value.
+        /// </summary>
+        [Test]
+        public void TestEnumValueSerialization()
+        {
+            CheckSerializeDeserialize(ByteEnum.Foo);
+            CheckSerializeDeserialize(ByteEnum.Bar);
+            CheckSerializeDeserialize(SByteEnum.Foo);
+            CheckSerializeDeserialize(SByteEnum.Bar);
 
+            CheckSerializeDeserialize(ShortEnum.Foo);
+            CheckSerializeDeserialize(ShortEnum.Bar);
+            CheckSerializeDeserialize(UShortEnum.Foo);
+            CheckSerializeDeserialize(UShortEnum.Bar);
+
+            CheckSerializeDeserialize(IntEnum.Foo);
+            CheckSerializeDeserialize(IntEnum.Bar);
+            CheckSerializeDeserialize(UIntEnum.Foo);
+            CheckSerializeDeserialize(UIntEnum.Bar);
+
+            CheckSerializeDeserialize(LongEnum.Foo);
+            CheckSerializeDeserialize(LongEnum.Bar);
+            CheckSerializeDeserialize(ULongEnum.Foo);
+            CheckSerializeDeserialize(ULongEnum.Bar);
+        }
+
+        /// <summary>
+        /// Tests enums arrays serialization.
+        /// </summary>
+        [Test]
+        public void TestEnumArraysSerialization()
+        {
+            CheckSerializeDeserialize(new[] {ByteEnum.Foo, ByteEnum.Bar});
+            CheckSerializeDeserialize(new[] {SByteEnum.Foo, SByteEnum.Bar});
+            CheckSerializeDeserialize(new Enum[] {ByteEnum.Foo, SByteEnum.Bar});
+
+            CheckSerializeDeserialize(new[] {ShortEnum.Foo, ShortEnum.Bar});
+            CheckSerializeDeserialize(new[] {UShortEnum.Foo, UShortEnum.Bar});
+            CheckSerializeDeserialize(new Enum[] {ShortEnum.Foo, UShortEnum.Bar});
+
+            CheckSerializeDeserialize(new[] {IntEnum.Foo, IntEnum.Bar});
+            CheckSerializeDeserialize(new[] {UIntEnum.Foo, UIntEnum.Bar});
+            CheckSerializeDeserialize(new Enum[] {IntEnum.Foo, UIntEnum.Bar});
+
+            CheckSerializeDeserialize(new[] {LongEnum.Foo, LongEnum.Bar});
+            CheckSerializeDeserialize(new[] {ULongEnum.Foo, ULongEnum.Bar});
+            CheckSerializeDeserialize(new Enum[] {LongEnum.Foo, ULongEnum.Bar});
+
+            CheckSerializeDeserialize(new Enum[]
+            {
+                ByteEnum.Foo, ByteEnum.Bar,
+                SByteEnum.Foo, SByteEnum.Bar,
+                ShortEnum.Foo, ShortEnum.Bar,
+                UShortEnum.Foo, ShortEnum.Bar,
+                IntEnum.Foo, IntEnum.Bar,
+                UIntEnum.Foo, UIntEnum.Bar,
+                LongEnum.Foo, LongEnum.Bar,
+                ULongEnum.Foo, LongEnum.Bar,
+            });
+        }
+
+        /// <summary>
+        /// Tests enums serialization when declared as System.Enum.
+        /// </summary>
+        [Test]
+        public void TestSystemEnumHolders()
+        {
+            CheckSerializeDeserialize(new RawEnumsHolder());
+            CheckSerializeDeserialize(new [] {new RawEnumsHolder(), new RawEnumsHolder()});
+            CheckSerializeDeserialize(new RawEnumsHolder2());
+            CheckSerializeDeserialize(new [] {new RawEnumsHolder2(), new RawEnumsHolder2()});
+            CheckSerializeDeserialize(new [] {new RawEnumsHolder(), new RawEnumsHolder2()});
+        }
+        
         private enum ByteEnum : byte
         {
             Foo = byte.MinValue,
@@ -550,5 +625,74 @@
                 info.AddValue("ulong", ULong);
             }
         }
+
+        /// <summary>
+        /// Holds enums declared as System.Enum.
+        /// </summary>
+        #pragma warning disable 659
+        private class RawEnumsHolder
+        {
+            private Enum EnmByteRaw { get; set; } = ByteEnum.Bar;
+            private Enum EnmUByteRaw { get; set; } = SByteEnum.Foo;
+            private Enum EnmShortRaw { get; set; } = ShortEnum.Bar;
+            private Enum EnmUShortRaw { get; set; } = UShortEnum.Foo;
+            private Enum EnmIntRaw { get; set; } = IntEnum.Bar;
+            private Enum EnmUIntRaw { get; set; } = UIntEnum.Foo;
+            private Enum EnmLongRaw { get; set; } = LongEnum.Bar;
+            private Enum EnmULongRaw { get; set; } = ULongEnum.Bar;
+
+            private Enum[] EnmRawArr { get; set; } = new Enum[]
+            {
+                ByteEnum.Bar, SByteEnum.Foo, ShortEnum.Bar,
+                UShortEnum.Foo, IntEnum.Bar, UIntEnum.Foo, LongEnum.Bar, ULongEnum.Foo
+            };
+
+            public override bool Equals(object obj)
+            {
+                if (ReferenceEquals(null, obj)) return false;
+                if (ReferenceEquals(this, obj)) return true;
+                if (obj.GetType() != this.GetType()) return false;
+
+                var other = (RawEnumsHolder) obj;
+
+                return Equals(EnmByteRaw, other.EnmByteRaw) && Equals(EnmUByteRaw, other.EnmUByteRaw) &&
+                       Equals(EnmShortRaw, other.EnmShortRaw) && Equals(EnmUShortRaw, other.EnmUShortRaw) &&
+                       Equals(EnmIntRaw, other.EnmIntRaw) && Equals(EnmUIntRaw, other.EnmUIntRaw) &&
+                       Equals(EnmLongRaw, other.EnmLongRaw) && Equals(EnmULongRaw, other.EnmULongRaw) &&
+                       Enumerable.SequenceEqual(EnmRawArr, other.EnmRawArr);
+            }
+        }
+        
+        /// <summary>
+        /// Additionally holds enums holder as field mixed with other simple fields and enums.
+        /// </summary>
+        #pragma warning disable 659
+        private class RawEnumsHolder2 : RawEnumsHolder
+        {
+            private RawEnumsHolder EnmHolder { get; set; } = new RawEnumsHolder();
+
+            private object Holder { get; set; } = new RawEnumsHolder();
+
+            private int IntVal { get; set; } = 1;
+
+            private string StringVal { get; set; } = "Foo";
+
+            private Enum EnmVal1 { get; set; } = LongEnum.Bar;
+
+            private IntEnum EnmVal2 { get; set; } = IntEnum.Foo;
+
+            public override bool Equals(object obj)
+            {
+                if (ReferenceEquals(null, obj)) return false;
+                if (ReferenceEquals(this, obj)) return true;
+                if (obj.GetType() != this.GetType()) return false;
+
+                var other = (RawEnumsHolder2) obj;
+
+                return base.Equals(other) && Equals(EnmHolder, other.EnmHolder) && Equals(Holder, other.Holder) &&
+                       IntVal == other.IntVal && StringVal == other.StringVal && Equals(EnmVal1, other.EnmVal1) &&
+                       EnmVal2 == other.EnmVal2;
+            }
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheWithPersistenceTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheWithPersistenceTest.cs
new file mode 100644
index 0000000..3dfa67c
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheWithPersistenceTest.cs
@@ -0,0 +1,239 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Tests.Cache.Platform
+{
+    using System;
+    using System.IO;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Core.Cache.Query;
+    using Apache.Ignite.Core.Configuration;
+    using Apache.Ignite.Core.Impl.Cache;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests platform cache with native persistence.
+    /// </summary>
+    public class PlatformCacheWithPersistenceTest
+    {
+        /** Cache name. */
+        private const string CacheName = "persistentCache";
+
+        /** Entry count. */
+        private const int Count = 100;
+
+        /** Temp dir for WAL. */
+        private readonly string _tempDir = PathUtils.GetTempDirectoryName();
+
+        /** Cache. */
+        private ICache<int,int> _cache;
+
+        /** Current key. Every test needs a key that was not used before. */
+        private int _key = 1;
+
+        /// <summary>
+        /// Sets up the test.
+        /// </summary>
+        [TestFixtureSetUp]
+        public void FixtureSetUp()
+        {
+            TestUtils.ClearWorkDir();
+
+            // Start Ignite, put data, stop.
+            using (var ignite = StartServer())
+            {
+                var cache = ignite.GetCache<int, int>(CacheName);
+
+                cache.PutAll(Enumerable.Range(1, Count).ToDictionary(x => x, x => x));
+
+                Assert.AreEqual(Count, cache.GetSize());
+                Assert.AreEqual(Count, cache.GetLocalSize(CachePeekMode.Platform));
+            }
+
+            // Start again to test persistent data restore.
+            var ignite2 = StartServer();
+            _cache = ignite2.GetCache<int, int>(CacheName);
+
+            // Platform cache is empty initially, because all entries are only on disk.
+            Assert.AreEqual(Count, _cache.GetSize());
+            Assert.AreEqual(0, _cache.GetLocalSize(CachePeekMode.Platform));
+        }
+
+        /// <summary>
+        /// Tears down the test.
+        /// </summary>
+        [TestFixtureTearDown]
+        public void TearDown()
+        {
+            Ignition.StopAll(true);
+
+            if (Directory.Exists(_tempDir))
+            {
+                Directory.Delete(_tempDir, true);
+            }
+
+            TestUtils.ClearWorkDir();
+        }
+
+        /// <summary>
+        /// Tests get operation.
+        /// </summary>
+        [Test]
+        public void TestGetRestoresPlatformCacheDataFromPersistence()
+        {
+            TestCacheOperation(k => _cache.Get(k));
+        }
+
+        /// <summary>
+        /// Tests getAll operation.
+        /// </summary>
+        [Test]
+        public void TestGetAllRestoresPlatformCacheDataFromPersistence()
+        {
+            TestCacheOperation(k => _cache.GetAll(new[] { k }));
+        }
+
+        /// <summary>
+        /// Tests containsKey operation.
+        /// </summary>
+        [Test]
+        public void TestContainsKeyRestoresPlatformCacheDataFromPersistence()
+        {
+            TestCacheOperation(k => _cache.ContainsKey(k));
+        }
+
+        /// <summary>
+        /// Tests containsKeys operation.
+        /// </summary>
+        [Test]
+        public void TestContainsKeysRestoresPlatformCacheDataFromPersistence()
+        {
+            TestCacheOperation(k => _cache.ContainsKeys(new[] { k }));
+        }
+
+        /// <summary>
+        /// Tests put operation.
+        /// </summary>
+        [Test]
+        public void TestPutUpdatesPlatformCache()
+        {
+            TestCacheOperation(k => _cache.Put(k, -k), k => -k);
+        }
+
+        /// <summary>
+        /// Tests getAndPut operation.
+        /// </summary>
+        [Test]
+        public void TestGetAndPutUpdatesPlatformCache()
+        {
+            TestCacheOperation(k => _cache.GetAndPut(k, -k), k => -k);
+        }
+
+        /// <summary>
+        /// Tests that local partition scan optimization is disabled when persistence is enabled.
+        /// See <see cref="CacheImpl{TK,TV}.ScanPlatformCache"/>.
+        /// </summary>
+        [Test]
+        public void TestScanQueryLocalPartitionScanOptimizationDisabledWithPersistence()
+        {
+            var res = _cache.Query(new ScanQuery<int, int>()).GetAll();
+            Assert.AreEqual(Count, res.Count);
+
+            var resLocal = _cache.Query(new ScanQuery<int, int> { Local = true }).GetAll();
+            Assert.AreEqual(Count, resLocal.Count);
+
+            var resPartition = _cache.Query(new ScanQuery<int, int> { Partition = 99 }).GetAll();
+            Assert.AreEqual(1, resPartition.Count);
+
+            var resLocalPartition = _cache.Query(new ScanQuery<int, int> { Local = true, Partition = 99 }).GetAll();
+            Assert.AreEqual(1, resLocalPartition.Count);
+        }
+
+        /// <summary>
+        /// Tests that scan query with filter does not need platform cache data to return correct results.
+        /// </summary>
+        [Test]
+        public void TestScanQueryWithFilterUsesPersistentData()
+        {
+            var res = _cache.Query(new ScanQuery<int, int> { Filter = new EvenValueFilter() }).GetAll();
+            Assert.AreEqual(Count / 2, res.Count);
+        }
+
+        /// <summary>
+        /// Starts the node.
+        /// </summary>
+        private IIgnite StartServer()
+        {
+            var ignite = Ignition.Start(GetIgniteConfiguration());
+
+            ignite.GetCluster().SetActive(true);
+
+            return ignite;
+        }
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        private IgniteConfiguration GetIgniteConfiguration()
+        {
+            return new IgniteConfiguration(TestUtils.GetTestConfiguration())
+            {
+                DataStorageConfiguration = new DataStorageConfiguration
+                {
+                    StoragePath = Path.Combine(_tempDir, "Store"),
+                    WalPath = Path.Combine(_tempDir, "WalStore"),
+                    WalArchivePath = Path.Combine(_tempDir, "WalArchive"),
+                    DefaultDataRegionConfiguration = new DataRegionConfiguration
+                    {
+                        Name = DataStorageConfiguration.DefaultDataRegionName,
+                        PersistenceEnabled = true
+                    }
+                },
+                CacheConfiguration = new[]
+                {
+                    new CacheConfiguration
+                    {
+                        Name = CacheName,
+                        PlatformCacheConfiguration = new PlatformCacheConfiguration()
+                    }
+                }
+            };
+        }
+
+        /// <summary>
+        /// Tests that cache operation causes platform cache to be populated for the key.
+        /// </summary>
+        private void TestCacheOperation(Action<int> operation, Func<int, int> expectedFunc = null)
+        {
+            var k = _key++;
+            operation(k);
+
+            var expected = expectedFunc == null ? k : expectedFunc(k);
+            Assert.AreEqual(expected, _cache.LocalPeek(k, CachePeekMode.Platform));
+        }
+
+        private class EvenValueFilter : ICacheEntryFilter<int, int>
+        {
+            public bool Invoke(ICacheEntry<int, int> entry)
+            {
+                return entry.Value % 2 == 0;
+            }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheDmlQueriesTestSchema.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheDmlQueriesTestSchema.cs
new file mode 100644
index 0000000..f0f5611
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheDmlQueriesTestSchema.cs
@@ -0,0 +1,255 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Tests.Cache.Query
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache.Query;
+    using Apache.Ignite.Core.Common;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests Data Manipulation Language queries related to schema.
+    /// </summary>
+    public class CacheDmlQueriesTestSchema
+    {
+        /// Table name
+        private const string TableName = "T1";
+
+        private const string SchemaName1 = "SCHEMA_1";
+        private const string SchemaName2 = "SCHEMA_2";
+        private const string SchemaName3 = "ScHeMa3";
+        private const string SchemaName4 = "SCHEMA_4";
+
+        private const string QSchemaName4 = "\"" + SchemaName3 + "\"";
+
+        /// <summary>
+        /// Sets up test fixture.
+        /// </summary>
+        [TestFixtureSetUp]
+        public void FixtureSetUp()
+        {
+            var cfg = new IgniteConfiguration(TestUtils.GetTestConfiguration())
+            {
+                SqlSchemas = new List<string>
+                {
+                    SchemaName1,
+                    SchemaName2,
+                    QSchemaName4,
+                    SchemaName4
+                }
+            };
+
+            Ignition.Start(cfg);
+        }
+
+        /// <summary>
+        /// Tears down test fixture.
+        /// </summary>
+        [TestFixtureTearDown]
+        public void FixtureTearDown()
+        {
+            Ignition.StopAll(true);
+        }
+
+        /// <summary>
+        /// Schema explicitly defined.
+        /// </summary>
+        [Test]
+        public void TestBasicOpsExplicitPublicSchema()
+        {
+            ExecuteStmtsAndVerify(() => true);
+        }
+
+        /// <summary>
+        /// Schema is imlicit.
+        /// </summary>
+        [Test]
+        public void TestBasicOpsImplicitPublicSchema()
+        {
+            ExecuteStmtsAndVerify(() => false);
+        }
+
+        /// <summary>
+        /// Schema is mixed.
+        /// </summary>
+        [Test]
+        public void TestBasicOpsMixedPublicSchema()
+        {
+            int i = 0;
+
+            ExecuteStmtsAndVerify(() => ((++i & 1) == 0));
+        }
+
+        /// <summary>
+        /// Create and drop non-existing schema.
+        /// </summary>
+        [Test]
+        public void TestCreateDropNonExistingSchema()
+        {
+            Assert.Throws<IgniteException>(() =>
+                Sql("CREATE TABLE UNKNOWN_SCHEMA." + TableName + "(id INT PRIMARY KEY, val INT)"));
+
+            Assert.Throws<IgniteException>(() => Sql("DROP TABLE UNKNOWN_SCHEMA." + TableName));
+        }
+
+        /// <summary>
+        /// Create tables in different schemas for same cache.
+        /// </summary>
+        [Test]
+        public void TestCreateTblsInDiffSchemasForSameCache()
+        {
+            const string testCache = "cache1";
+
+            Sql("CREATE TABLE " + SchemaName1 + '.' + TableName
+                + " (s1_key INT PRIMARY KEY, s1_val INT) WITH \"cache_name=" + testCache + "\"");
+
+            Assert.Throws<IgniteException>(
+                () => Sql("CREATE TABLE " + SchemaName2 + '.' + TableName
+                          + " (s1_key INT PRIMARY KEY, s2_val INT) WITH \"cache_name=" + testCache + "\"")
+            );
+
+            Sql("DROP TABLE " + SchemaName1 + '.' + TableName);
+        }
+
+        /// <summary>
+        /// Basic test with different schemas.
+        /// </summary>
+        [Test]
+        public void TestBasicOpsDiffSchemas()
+        {
+            Sql("CREATE TABLE " + SchemaName1 + '.' + TableName + " (s1_key INT PRIMARY KEY, s1_val INT)");
+            Sql("CREATE TABLE " + SchemaName2 + '.' + TableName + " (s2_key INT PRIMARY KEY, s2_val INT)");
+            Sql("CREATE TABLE " + QSchemaName4 + '.' + TableName + " (s3_key INT PRIMARY KEY, s3_val INT)");
+            Sql("CREATE TABLE " + SchemaName4 + '.' + TableName + " (s4_key INT PRIMARY KEY, s4_val INT)");
+
+            Sql("INSERT INTO " + SchemaName1 + '.' + TableName + " (s1_key, s1_val) VALUES (1, 2)");
+            Sql("INSERT INTO " + SchemaName2 + '.' + TableName + " (s2_key, s2_val) VALUES (1, 2)");
+            Sql("INSERT INTO " + QSchemaName4 + '.' + TableName + " (s3_key, s3_val) VALUES (1, 2)");
+            Sql("INSERT INTO " + SchemaName4 + '.' + TableName + " (s4_key, s4_val) VALUES (1, 2)");
+
+            Sql("UPDATE " + SchemaName1 + '.' + TableName + " SET s1_val = 5");
+            Sql("UPDATE " + SchemaName2 + '.' + TableName + " SET s2_val = 5");
+            Sql("UPDATE " + QSchemaName4 + '.' + TableName + " SET s3_val = 5");
+            Sql("UPDATE " + SchemaName4 + '.' + TableName + " SET s4_val = 5");
+
+            Sql("DELETE FROM " + SchemaName1 + '.' + TableName);
+            Sql("DELETE FROM " + SchemaName2 + '.' + TableName);
+            Sql("DELETE FROM " + QSchemaName4 + '.' + TableName);
+            Sql("DELETE FROM " + SchemaName4 + '.' + TableName);
+
+            Sql("CREATE INDEX t1_idx_1 ON " + SchemaName1 + '.' + TableName + "(s1_val)");
+            Sql("CREATE INDEX t1_idx_1 ON " + SchemaName2 + '.' + TableName + "(s2_val)");
+            Sql("CREATE INDEX t1_idx_1 ON " + QSchemaName4 + '.' + TableName + "(s3_val)");
+            Sql("CREATE INDEX t1_idx_1 ON " + SchemaName4 + '.' + TableName + "(s4_val)");
+
+            Sql("SELECT * FROM " + SchemaName1 + '.' + TableName);
+            Sql("SELECT * FROM " + SchemaName2 + '.' + TableName);
+            Sql("SELECT * FROM " + QSchemaName4 + '.' + TableName);
+            Sql("SELECT * FROM " + SchemaName4 + '.' + TableName);
+
+            Sql("SELECT * FROM " + SchemaName1 + '.' + TableName
+                + " JOIN " + SchemaName2 + '.' + TableName
+                + " JOIN " + QSchemaName4 + '.' + TableName
+                + " JOIN " + SchemaName4 + '.' + TableName);
+
+            VerifyTables();
+
+            Sql("DROP TABLE " + SchemaName1 + '.' + TableName);
+            Sql("DROP TABLE " + SchemaName2 + '.' + TableName);
+            Sql("DROP TABLE " + QSchemaName4 + '.' + TableName);
+            Sql("DROP TABLE " + SchemaName4 + '.' + TableName);
+        }
+
+        /// <summary>
+        /// Verify tables.
+        /// </summary>
+        private static void VerifyTables()
+        {
+            Sql("SELECT SCHEMA_NAME, KEY_ALIAS FROM SYS.TABLES ORDER BY SCHEMA_NAME", res =>
+            {
+                Assert.AreEqual(new List<List<object>>
+                {
+                    new List<object> {SchemaName1, "S1_KEY"},
+                    new List<object> {SchemaName2, "S2_KEY"},
+                    new List<object> {SchemaName4, "S4_KEY"},
+                    new List<object> {SchemaName3, "S3_KEY"}
+                }, res);
+            });
+        }
+
+        /// <summary>
+        /// Get test table name.
+        /// </summary>
+        private static string GetTableName(bool withSchema)
+        {
+            return withSchema ? "PUBLIC." + TableName : TableName;
+        }
+
+        /// <summary>
+        /// Perform SQL query.
+        /// </summary>
+        private static void Sql(string qry, Action<IList<IList<object>>> validator = null)
+        {
+            var res = Ignition
+                .GetIgnite()
+                .GetOrCreateCache<int, int>("TestCache")
+                .Query(new SqlFieldsQuery(qry))
+                .GetAll();
+
+            if (validator != null)
+                validator.Invoke(res);
+        }
+
+        /// <summary>
+        /// Generate one-row result set.
+        /// </summary>
+        private static List<List<object>> OneRowList(params object[] vals)
+        {
+            return new List<List<object>> {vals.ToList()};
+        }
+
+        /// <summary>
+        /// Create/insert/update/delete/drop table in PUBLIC schema.
+        /// </summary>
+        private static void ExecuteStmtsAndVerify(Func<bool> withSchemaDecisionSup)
+        {
+            Sql("CREATE TABLE " + GetTableName(withSchemaDecisionSup()) + " (id INT PRIMARY KEY, val INT)");
+
+            Sql("CREATE INDEX t1_idx_1 ON " + GetTableName(withSchemaDecisionSup()) + "(val)");
+
+            Sql("INSERT INTO " + GetTableName(withSchemaDecisionSup()) + " (id, val) VALUES(1, 2)");
+            Sql("SELECT * FROM " + GetTableName(withSchemaDecisionSup()),
+                res => Assert.AreEqual(OneRowList(1, 2), res));
+
+            Sql("UPDATE " + GetTableName(withSchemaDecisionSup()) + " SET val = 5");
+            Sql("SELECT * FROM " + GetTableName(withSchemaDecisionSup()),
+                res => Assert.AreEqual(OneRowList(1, 5), res));
+
+            Sql("DELETE FROM " + GetTableName(withSchemaDecisionSup()) + " WHERE id = 1");
+            Sql("SELECT COUNT(*) FROM " + GetTableName(withSchemaDecisionSup()),
+                res => Assert.AreEqual(OneRowList(0), res));
+
+            Sql("SELECT COUNT(*) FROM SYS.TABLES WHERE schema_name = 'PUBLIC' " +
+                "AND table_name = \'" + TableName + "\'", res => Assert.AreEqual(OneRowList(1), res));
+
+            Sql("DROP TABLE " + GetTableName(withSchemaDecisionSup()));
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
index b7c65bb..92501b9 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
@@ -34,6 +34,7 @@
     using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.Log;
     using Apache.Ignite.Core.Tests.Client.Cache;
+    using Apache.Ignite.Core.Tests.Client.Compute;
     using NUnit.Framework;
 
     /// <summary>
@@ -643,6 +644,216 @@
         }
 
         /// <summary>
+        /// Tests automatic retry with one server.
+        /// </summary>
+        [Test]
+        public void TestFailoverWithRetryPolicyReconnectsToNewNode()
+        {
+            Ignition.Start(TestUtils.GetTestConfiguration());
+
+            var cfg = new IgniteClientConfiguration
+            {
+                Endpoints = new[] { "127.0.0.1" },
+                RetryPolicy = new ClientRetryReadPolicy()
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                var restartTask = Task.Run(() =>
+                {
+                    Ignition.StopAll(true);
+                    Thread.Sleep(100);
+                    Ignition.Start(TestUtils.GetTestConfiguration());
+                });
+
+                while (!restartTask.IsCompleted)
+                {
+                    // Operations do not fail while the only node is being restarted.
+                    Assert.AreEqual(0, client.GetCacheNames().Count);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Tests that operation fails with an exception when retry limit is reached.
+        /// </summary>
+        [Test]
+        public void TestFailoverWithRetryPolicyThrowsOnRetryCountExceeded()
+        {
+            Ignition.Start(TestUtils.GetTestConfiguration());
+
+            var retryLimit = 4;
+
+            var cfg = new IgniteClientConfiguration
+            {
+                Endpoints = new[] { "127.0.0.1" },
+                RetryPolicy = new ClientRetryAllPolicy(),
+                RetryLimit = retryLimit
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                Assert.AreEqual(0, client.GetCacheNames().Count);
+
+                Ignition.StopAll(true);
+
+                var ex = Assert.Throws<IgniteClientException>(() => client.GetCacheNames());
+                StringAssert.StartsWith($"Operation failed after {retryLimit} retries", ex.Message);
+
+                Assert.IsNotNull(ex.InnerException);
+                Assert.IsInstanceOf<AggregateException>(ex.InnerException);
+                Assert.AreEqual(retryLimit, ((AggregateException)ex.InnerException).InnerExceptions.Count);
+            }
+        }
+
+        /// <summary>
+        /// Tests that failed operations not related to connection issues are not retried.
+        /// </summary>
+        [Test]
+        public void TestFailoverWithRetryPolicyDoesNotRetryUnrelatedErrors()
+        {
+            Ignition.Start(TestUtils.GetTestConfiguration());
+
+            var cfg = new IgniteClientConfiguration
+            {
+                Endpoints = new[] { "127.0.0.1" },
+                RetryPolicy = new ClientRetryAllPolicy()
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                var ex = Assert.Catch<Exception>(() =>
+                    client.GetCompute().ExecuteJavaTask<object>(ComputeClientTests.TestTask, null));
+
+                StringAssert.StartsWith(
+                    "Compute grid functionality is disabled for thin clients", ex.GetInnermostException().Message);
+            }
+        }
+
+        /// <summary>
+        /// Tests automatic retry with multiple servers.
+        /// </summary>
+        [Test]
+        public void TestFailoverWithRetryPolicyCompletesOperationWithoutException(
+            [Values(true, false)] bool async,
+            [Values(true, false)] bool partitionAware)
+        {
+            // Start 3 nodes.
+            Func<string, IgniteConfiguration> getConfig = name =>
+                new IgniteConfiguration(TestUtils.GetTestConfiguration(name: name))
+                {
+                    ClientConnectorConfiguration = new ClientConnectorConfiguration
+                    {
+                        ThinClientConfiguration = new ThinClientConfiguration
+                        {
+                            MaxActiveComputeTasksPerConnection = 1
+                        }
+                    }
+                };
+
+            Ignition.Start(getConfig("0"));
+            Ignition.Start(getConfig("1"));
+            Ignition.Start(getConfig("2"));
+
+            // Connect client.
+            var port = IgniteClientConfiguration.DefaultPort;
+            var cfg = new IgniteClientConfiguration
+            {
+                Endpoints = new[]
+                {
+                    "localhost",
+                    string.Format("127.0.0.1:{0}..{1}", port + 1, port + 2)
+                },
+                RetryPolicy = new ClientRetryAllPolicy(),
+                RetryLimit = 3,
+                EnablePartitionAwareness = partitionAware
+            };
+
+            // ReSharper disable AccessToDisposedClosure
+            using (var client = Ignition.StartClient(cfg))
+            {
+                var cache = client.GetOrCreateCache<int, int>("c");
+
+                // Check all DoOp overloads.
+                Action checkOperation = partitionAware
+                    ? async
+                        ? (Action)(() => Assert.IsFalse(cache.ContainsKeyAsync(1).Result))
+                        : () => Assert.IsFalse(cache.ContainsKey(1))
+                    : async
+                        ? (Action)(() => Assert.IsNotNull(client.GetCompute().ExecuteJavaTaskAsync<object>(
+                            ComputeClientTests.TestTask, null).Result))
+                        : () => Assert.AreEqual(1, client.GetCacheNames().Count);
+
+                checkOperation();
+
+                // Stop first node.
+                var nodeId = ((IPEndPoint) client.RemoteEndPoint).Port - port;
+                Ignition.Stop(nodeId.ToString(), true);
+
+                checkOperation();
+
+                // Stop second node.
+                nodeId = ((IPEndPoint) client.RemoteEndPoint).Port - port;
+                Ignition.Stop(nodeId.ToString(), true);
+
+                checkOperation();
+
+                // Stop all nodes.
+                Ignition.StopAll(true);
+
+                Assert.IsNotNull(GetSocketException(Assert.Catch(() => client.GetCacheNames())));
+                Assert.IsNotNull(GetSocketException(Assert.Catch(() => client.GetCacheNames())));
+            }
+        }
+
+        /// <summary>
+        /// Tests custom retry policy.
+        /// </summary>
+        [Test]
+        public void TestCustomRetryPolicyIsInvokedWithCorrectContext()
+        {
+            Ignition.Start(TestUtils.GetTestConfiguration());
+
+            var retryPolicy = new TestRetryPolicy(ClientOperationType.CacheGetNames);
+
+            var cfg = new IgniteClientConfiguration
+            {
+                Endpoints = new[] { "127.0.0.1" },
+                RetryPolicy = retryPolicy,
+                RetryLimit = 2
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                Assert.AreEqual(0, client.GetCacheNames().Count);
+
+                Ignition.StopAll(true);
+
+                var errorWithoutRetry = GetSocketException(Assert.Catch(() => client.GetCluster().GetNodes()));
+                var errorWithRetry = Assert.Throws<IgniteClientException>(() => client.GetCacheNames());
+
+                Assert.IsNotNull(errorWithoutRetry);
+                StringAssert.StartsWith("Operation failed after 2 retries", errorWithRetry.Message);
+
+                Assert.AreEqual(3, retryPolicy.Invocations.Count);
+
+                Assert.AreEqual(ClientOperationType.ClusterGroupGetNodes, retryPolicy.Invocations[0].Operation);
+                Assert.AreEqual(0, retryPolicy.Invocations[0].Iteration);
+                Assert.AreSame(retryPolicy, retryPolicy.Invocations[0].Configuration.RetryPolicy);
+                Assert.AreEqual(2, retryPolicy.Invocations[0].Configuration.RetryLimit);
+                Assert.IsInstanceOf<SocketException>(retryPolicy.Invocations[0].Exception.GetBaseException());
+
+                Assert.AreEqual(ClientOperationType.CacheGetNames, retryPolicy.Invocations[1].Operation);
+                Assert.AreEqual(0, retryPolicy.Invocations[1].Iteration);
+                Assert.IsInstanceOf<SocketException>(retryPolicy.Invocations[1].Exception.GetBaseException());
+
+                Assert.AreEqual(ClientOperationType.CacheGetNames, retryPolicy.Invocations[2].Operation);
+                Assert.AreEqual(1, retryPolicy.Invocations[2].Iteration);
+                Assert.IsInstanceOf<SocketException>(retryPolicy.Invocations[2].Exception.GetBaseException());
+            }
+        }
+
+        /// <summary>
         /// Tests that client stops it's receiver thread upon disposal.
         /// </summary>
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
index b2f2e0e..9e9c4e3 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
@@ -62,6 +62,9 @@
         /** Server list log levels. */
         private readonly LogLevel[] _serverListLoggerLevels;
 
+        /** */
+        protected readonly bool _useBinaryArray;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ClientTestBase"/> class.
         /// </summary>
@@ -78,13 +81,15 @@
             bool enableSsl = false,
             bool enablePartitionAwareness = false,
             bool enableServerListLogging = false,
-            LogLevel[] serverListLoggerLevels = null)
+            LogLevel[] serverListLoggerLevels = null,
+            bool useBinaryArray = false)
         {
             _gridCount = gridCount;
             _enableSsl = enableSsl;
             _enablePartitionAwareness = enablePartitionAwareness;
             _enableServerListLogging = enableServerListLogging;
             _serverListLoggerLevels = serverListLoggerLevels ?? new[] { LogLevel.Debug, LogLevel.Warn, LogLevel.Error };
+            _useBinaryArray = useBinaryArray;
         }
 
         /// <summary>
@@ -222,7 +227,8 @@
                     }
                     : new TestUtils.TestContextLogger(),
                 SpringConfigUrl = _enableSsl ? Path.Combine("Config", "Client", "server-with-ssl.xml") : null,
-                RedirectJavaConsoleOutput = false
+                RedirectJavaConsoleOutput = false,
+                LifecycleHandlers = _useBinaryArray ? new[] { new SetUseBinaryArray() } : null
             };
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compatibility/ClientProtocolCompatibilityTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compatibility/ClientProtocolCompatibilityTest.cs
index 6fa058c..e6ca434 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compatibility/ClientProtocolCompatibilityTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compatibility/ClientProtocolCompatibilityTest.cs
@@ -201,7 +201,7 @@
         /// </summary>
         internal static void AssertNotSupportedFeatureOperation(Action action, ClientBitmaskFeature feature, ClientOp op)
         {
-            var ex = Assert.Throws<IgniteClientException>(() => action());
+            var ex = Assert.Catch<Exception>(() => action()).GetBaseException();
 
             var expectedMessage = string.Format(
                 "Operation {0} is not supported by the server. " +
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compute/ComputeClientTests.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compute/ComputeClientTests.cs
index adce7b0..3efdd46 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compute/ComputeClientTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Compute/ComputeClientTests.cs
@@ -40,7 +40,7 @@
     public class ComputeClientTests : ClientTestBase
     {
         /** */
-        private const string TestTask = "org.apache.ignite.internal.client.thin.TestTask";
+        public const string TestTask = "org.apache.ignite.internal.client.thin.TestTask";
 
         /** */
         private const string TestResultCacheTask = "org.apache.ignite.internal.client.thin.TestResultCacheTask";
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/IgniteClientConfigurationTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/IgniteClientConfigurationTest.cs
index 1aacaf2..3a09e4b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/IgniteClientConfigurationTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/IgniteClientConfigurationTest.cs
@@ -300,7 +300,6 @@
             }
         }
 
-#if !NETCOREAPP
         /// <summary>
         /// Tests the schema validation.
         /// </summary>
@@ -324,7 +323,6 @@
                 "IgniteClientConfigurationSection.xsd", "igniteClientConfiguration",
                 typeof(IgniteClientConfiguration));
         }
-#endif
 
         /// <summary>
         /// Tests <see cref="TransactionClientConfiguration"/> copy ctor.
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ITestService.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ITestService.cs
index 4bfba68..9061f194 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ITestService.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ITestService.cs
@@ -62,5 +62,11 @@
 
         /** */
         Guid GetNodeId();
+        
+        /** */
+        string ContextAttribute(string name);
+        
+        /** */
+        byte[] ContextBinaryAttribute(string name);
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ServicesClientTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ServicesClientTest.cs
index f74b0b4..1b1d174 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ServicesClientTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/ServicesClientTest.cs
@@ -20,9 +20,11 @@
     using System;
     using System.Collections.Generic;
     using System.Linq;
+    using System.Text;
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Client;
     using Apache.Ignite.Core.Client.Services;
+    using Apache.Ignite.Core.Platform;
     using Apache.Ignite.Core.Services;
     using Apache.Ignite.Core.Tests.Client.Cache;
     using Apache.Ignite.Core.Tests.Services;
@@ -44,6 +46,12 @@
             // No-op.
         }
 
+        /** */
+        public ServicesClientTest(bool useBinaryArray) : base(2, useBinaryArray: useBinaryArray)
+        {
+            // No-op.
+        }
+
         /// <summary>
         /// Tears down the test.
         /// </summary>
@@ -305,8 +313,17 @@
             Assert.AreEqual(1, svc.Foo(default(uint)));
             Assert.AreEqual(4, svc.Foo(default(ushort)));
 
-            // Array types are not distinguished.
-            Assert.AreEqual(9, svc.Foo(new[] {new Person(0)}));
+            if (!_useBinaryArray)
+            {
+                // Array types are not distinguished.
+                Assert.AreEqual(9, svc.Foo(new[] {new Person(0)}));
+            }
+            else
+            {
+                Assert.AreEqual(9, svc.Foo(new object[] {new Person(0)}));
+                Assert.AreEqual(10, svc.Foo(new[] {new Person(0)}));
+                Assert.AreEqual(10, svc.Foo(new Person[] {new Person(0)}));
+            }
         }
 
         /// <summary>
@@ -385,12 +402,12 @@
             // Binary object.
             Assert.AreEqual(15,
                 binSvc.testBinaryObject(
-                    Client.GetBinary().ToBinary<IBinaryObject>(new ServicesTest.PlatformComputeBinarizable {Field = 6}))
+                    Client.GetBinary().ToBinary<IBinaryObject>(new PlatformComputeBinarizable {Field = 6}))
                     .GetField<int>("Field"));
 
             // Binary object array.
             var arr  = new[] {10, 11, 12}.Select(
-                x => new ServicesTest.PlatformComputeBinarizable {Field = x}).ToArray();
+                x => new PlatformComputeBinarizable {Field = x}).ToArray();
 
             var binArr = arr.Select(Client.GetBinary().ToBinary<IBinaryObject>).ToArray();
 
@@ -531,9 +548,7 @@
         [Test]
         public void TestNonExistentServiceNameCausesClientException()
         {
-            var svc = Client.GetServices().GetServiceProxy<ITestService>(ServiceName);
-
-            var ex = Assert.Throws<IgniteClientException>(() => svc.VoidMethod());
+            var ex = Assert.Throws<IgniteClientException>(() => Client.GetServices().GetServiceProxy<ITestService>(ServiceName));
             Assert.AreEqual(ClientStatusCode.Fail, ex.StatusCode);
         }
 
@@ -551,6 +566,64 @@
 
             Assert.AreEqual(1, task.Result);
         }
+        
+        /// <summary>
+        /// Tests custom caller context.
+        /// </summary>
+        [Test]
+        public void TestServiceCallContext()
+        {
+            string attrName = "attr";
+            string binAttrName = "binAttr";
+            string attrValue = "value";
+            byte[] binAttrValue = Encoding.UTF8.GetBytes(attrValue);
+            
+            IServiceCallContext callCtx = new ServiceCallContextBuilder()
+                .Set(attrName, attrValue)
+                .Set(binAttrName, binAttrValue)
+                .Build();
+
+            var svc = DeployAndGetTestService<ITestService>(null, callCtx);
+
+            Assert.AreEqual(attrValue, svc.ContextAttribute(attrName));
+            Assert.AreEqual(binAttrValue, svc.ContextBinaryAttribute(binAttrName));
+
+            Assert.Throws<ArgumentException>(() =>
+                DeployAndGetTestService<ITestService>(null, new CustomServiceCallContext()));
+        }           
+
+        [Test]
+        public void TestGetServiceDescriptors()
+        {
+            DeployAndGetTestService();
+
+            var svcs = Client.GetServices().GetServiceDescriptors();
+
+            Assert.AreEqual(1, svcs.Count);
+
+            var svc = svcs.First();
+
+            Assert.AreEqual(ServiceName, svc.Name);
+            Assert.AreEqual(
+                "org.apache.ignite.internal.processors.platform.dotnet.PlatformDotNetServiceImpl",
+                svc.ServiceClass
+            );
+            Assert.AreEqual(1, svc.TotalCount);
+            Assert.AreEqual(1, svc.MaxPerNodeCount);
+            Assert.IsNull(svc.CacheName);
+            Assert.AreEqual(Ignition.GetIgnite().GetCluster().GetLocalNode().Id, svc.OriginNodeId);
+            Assert.AreEqual(PlatformType.DotNet, svc.PlatformType);
+
+            var svc1 = Client.GetServices().GetServiceDescriptor(ServiceName);
+
+            Assert.AreEqual(svc.Name, svc1.Name);
+            Assert.AreEqual(svc.ServiceClass, svc1.ServiceClass);
+            Assert.AreEqual(svc.TotalCount, svc1.TotalCount);
+            Assert.AreEqual(svc.MaxPerNodeCount, svc1.MaxPerNodeCount);
+            Assert.AreEqual(svc.CacheName, svc1.CacheName);
+            Assert.AreEqual(svc.OriginNodeId, svc1.OriginNodeId);
+            Assert.AreEqual(svc.PlatformType, svc1.PlatformType);
+        }
 
         /// <summary>
         /// Deploys test service and returns client-side proxy.
@@ -563,7 +636,8 @@
         /// <summary>
         /// Deploys test service and returns client-side proxy.
         /// </summary>
-        private T DeployAndGetTestService<T>(Func<IServicesClient, IServicesClient> transform = null) where T : class
+        private T DeployAndGetTestService<T>(Func<IServicesClient, IServicesClient> transform = null, 
+            IServiceCallContext callCtx = null) where T : class
         {
             ServerServices.DeployClusterSingleton(ServiceName, new TestService());
 
@@ -574,7 +648,7 @@
                 services = transform(services);
             }
 
-            return services.GetServiceProxy<T>(ServiceName);
+            return services.GetServiceProxy<T>(ServiceName, callCtx);
         }
 
         /// <summary>
@@ -584,5 +658,35 @@
         {
             get { return Ignition.GetIgnite().GetServices(); }
         }
+        
+        /// <summary>
+        /// Custom implementation of the service call context.
+        /// </summary>
+        private class CustomServiceCallContext : IServiceCallContext
+        {
+            /** <inheritdoc /> */
+            public string GetAttribute(string name)
+            {
+                return null;
+            }
+
+            /** <inheritdoc /> */
+            public byte[] GetBinaryAttribute(string name)
+            {
+                return null;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Tests for <see cref="IServicesClient"/>.
+    /// </summary>
+    public class ServicesClientTestBinaryArrays : ServicesClientTest
+    {
+        /** */
+        public ServicesClientTestBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/TestService.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/TestService.cs
index c61b43a..1b80573 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/TestService.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Services/TestService.cs
@@ -34,6 +34,9 @@
         /** */
         [InstanceResource]
         private readonly IIgnite _ignite = null;
+        
+        /** Service context. */
+        private IServiceContext _ctx;
 
         /** */
         public const string ExceptionText = "Some error";
@@ -70,7 +73,8 @@
         {
             var tcs = new TaskCompletionSource<int>();
             new Timer(_ => tcs.SetResult(1)).Change(500, -1);
-            return tcs.Task;        }
+            return tcs.Task;
+        }
 
         /** <inheritdoc /> */
         public Person PersonMethod(Person person)
@@ -112,11 +116,27 @@
         {
             return _ignite.GetCluster().GetLocalNode().Id;
         }
+        
+        /** <inheritdoc /> */
+        public string ContextAttribute(string name)
+        {
+            IServiceCallContext callCtx = _ctx.CurrentCallContext;
+
+            return callCtx == null ? null : callCtx.GetAttribute(name);
+        }
+        
+        /** <inheritdoc /> */
+        public byte[] ContextBinaryAttribute(string name)
+        {
+            IServiceCallContext callCtx = _ctx.CurrentCallContext;
+
+            return callCtx == null ? null : callCtx.GetBinaryAttribute(name);
+        }
 
         /** <inheritdoc /> */
         public void Init(IServiceContext context)
         {
-            // No-op.
+            _ctx = context;
         }
 
         /** <inheritdoc /> */
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/TestRetryPolicy.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/TestRetryPolicy.cs
new file mode 100644
index 0000000..5f0c75d
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/TestRetryPolicy.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Tests.Client
+{
+    using System.Collections.Generic;
+    using System.Linq;
+    using Apache.Ignite.Core.Client;
+
+    /// <summary>
+    /// Test policy.
+    /// </summary>
+    public class TestRetryPolicy : IClientRetryPolicy
+    {
+        /** */
+        private readonly IReadOnlyCollection<ClientOperationType> _allowedOperations;
+
+        /** */
+        private readonly List<IClientRetryPolicyContext> _invocations = new List<IClientRetryPolicyContext>();
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="TestRetryPolicy"/> class.
+        /// </summary>
+        /// <param name="allowedOperations">A list of operation types to retry.</param>
+        public TestRetryPolicy(params ClientOperationType[] allowedOperations)
+        {
+            _allowedOperations = allowedOperations.ToArray();
+        }
+
+        /// <summary>
+        /// Gets the invocations.
+        /// </summary>
+        public IReadOnlyList<IClientRetryPolicyContext> Invocations => _invocations;
+
+        /** <inheritDoc /> */
+        public bool ShouldRetry(IClientRetryPolicyContext context)
+        {
+            _invocations.Add(context);
+
+            return _allowedOperations.Contains(context.Operation);
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/ComputeApiTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/ComputeApiTest.cs
index ec5750d..2bf41f2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/ComputeApiTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/ComputeApiTest.cs
@@ -1034,15 +1034,6 @@
         }
     }
 
-    class PlatformComputeBinarizable
-    {
-        public int Field
-        {
-            get;
-            set;
-        }
-    }
-
     class PlatformComputeNetBinarizable : PlatformComputeBinarizable
     {
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/IgniteExceptionTaskSelfTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/IgniteExceptionTaskSelfTest.cs
index 0af647b..6d819ef 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/IgniteExceptionTaskSelfTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Compute/IgniteExceptionTaskSelfTest.cs
@@ -242,11 +242,11 @@
         {
             _mode = ErrorMode.RmtResErrNotMarshalable;
 
-            BadException e = ExecuteWithError() as BadException;
+            var err = ExecuteWithError();
+            var badException = err as BadException;
 
-            Assert.IsNotNull(e);
-
-            Assert.AreEqual(ErrorMode.RmtResErrNotMarshalable, e.Mode);
+            Assert.IsNotNull(badException, err.ToString());
+            Assert.AreEqual(ErrorMode.RmtResErrNotMarshalable, badException.Mode);
         }
 
         /// <summary>
@@ -359,7 +359,7 @@
             RmtJobErrNotMarshalable,
 
             /** Remote job result is not marshalable. */
-            RmtJobResNotMarshalable,            
+            RmtJobResNotMarshalable,
 
             /** Error occurred during local result processing. */
             LocResErr,
@@ -489,7 +489,7 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         [Serializable]
         private class GoodJob : IComputeJob<object>, ISerializable
@@ -498,7 +498,7 @@
             private readonly bool _rmt;
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="rmt"></param>
             public GoodJob(bool rmt)
@@ -507,7 +507,7 @@
             }
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="info"></param>
             /// <param name="context"></param>
@@ -565,7 +565,7 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         private class BadJob : IComputeJob<object>, IBinarizable
         {
@@ -595,16 +595,16 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         [Serializable]
         private class GoodJobResult : ISerializable
         {
             /** */
             public readonly bool Rmt;
-            
+
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="rmt"></param>
             public GoodJobResult(bool rmt)
@@ -613,7 +613,7 @@
             }
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="info"></param>
             /// <param name="context"></param>
@@ -630,7 +630,7 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         private class BadJobResult : IBinarizable
         {
@@ -638,7 +638,7 @@
             public readonly bool Rmt;
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="rmt"></param>
             public BadJobResult(bool rmt)
@@ -660,7 +660,7 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         [Serializable]
         private class GoodTaskResult : ISerializable
@@ -669,7 +669,7 @@
             public readonly int Res;
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="res"></param>
             public GoodTaskResult(int res)
@@ -678,7 +678,7 @@
             }
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="info"></param>
             /// <param name="context"></param>
@@ -695,7 +695,7 @@
         }
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         private class BadTaskResult
         {
@@ -703,7 +703,7 @@
             public readonly int Res;
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="res"></param>
             public BadTaskResult(int res)
@@ -720,9 +720,9 @@
         {
             /** */
             public readonly ErrorMode Mode;
-            
+
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="mode"></param>
             public GoodException(ErrorMode mode)
@@ -731,7 +731,7 @@
             }
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="info"></param>
             /// <param name="context"></param>
@@ -758,7 +758,7 @@
             public readonly ErrorMode Mode;
 
             /// <summary>
-            /// 
+            ///
             /// </summary>
             /// <param name="mode"></param>
             public BadException(ErrorMode mode)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
index 41c5cfb..ccb267d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
@@ -163,8 +163,7 @@
     <dataStorageConfiguration alwaysWriteFullPages="false" checkpointFrequency="00:00:01"
                               checkpointThreads="3" concurrencyLevel="4" lockWaitTime="00:00:05" metricsEnabled="true"
                               pageSize="6" storagePath="cde" metricsRateTimeInterval="00:00:07"
-                              metricsSubIntervalCount="8" systemRegionInitialSize="9" systemRegionMaxSize="10"
-                              walThreadLocalBufferSize="11"
+                              metricsSubIntervalCount="8" walThreadLocalBufferSize="11"
                               walArchivePath="abc" walFlushFrequency="00:00:12" walFsyncDelayNanos="13" walHistorySize="14"
                               walMode="Background" walRecordIteratorBufferSize="15" walSegments="16" walSegmentSize="17"
                               walPath="wal-store" writeThrottlingEnabled="true" walAutoArchiveAfterInactivity="00:00:18"
@@ -179,6 +178,7 @@
                                         maxSize="5" metricsEnabled="false" name="reg1" pageEvictionMode="Disabled"
                                         metricsRateTimeInterval="00:00:03" metricsSubIntervalCount="6"
                                         swapPath="swap2" checkpointPageBufferSize="8" />
+        <systemDataRegionConfiguration initialSize="9" maxSize="10" />
     </dataStorageConfiguration>
     <sslContextFactory type='SslContextFactory' protocol='SSL'
                        keyStoreFilePath='KeyStore/server.jks' keyStorePassword='123456'
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
index 429cd48..fc4350e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
@@ -331,8 +331,6 @@
             Assert.AreEqual("cde", ds.StoragePath);
             Assert.AreEqual(TimeSpan.FromSeconds(7), ds.MetricsRateTimeInterval);
             Assert.AreEqual(8, ds.MetricsSubIntervalCount);
-            Assert.AreEqual(9, ds.SystemRegionInitialSize);
-            Assert.AreEqual(10, ds.SystemRegionMaxSize);
             Assert.AreEqual(11, ds.WalThreadLocalBufferSize);
             Assert.AreEqual("abc", ds.WalArchivePath);
             Assert.AreEqual(TimeSpan.FromSeconds(12), ds.WalFlushFrequency);
@@ -373,6 +371,10 @@
             Assert.AreEqual("swap2", dr.SwapPath);
             Assert.IsFalse(dr.MetricsEnabled);
 
+            var sysDr = ds.SystemDataRegionConfiguration;
+            Assert.AreEqual(9, sysDr.InitialSize);
+            Assert.AreEqual(10, sysDr.MaxSize);
+
             Assert.IsInstanceOf<SslContextFactory>(cfg.SslContextFactory);
 
             Assert.IsInstanceOf<StopNodeOrHaltFailureHandler>(cfg.FailureHandler);
@@ -1029,8 +1031,6 @@
                     MetricsRateTimeInterval = TimeSpan.FromSeconds(9),
                     CheckpointWriteOrder = Core.Configuration.CheckpointWriteOrder.Sequential,
                     WriteThrottlingEnabled = true,
-                    SystemRegionInitialSize = 64 * 1024 * 1024,
-                    SystemRegionMaxSize = 128 * 1024 * 1024,
                     ConcurrencyLevel = 1,
                     PageSize = 5 * 1024,
                     WalAutoArchiveAfterInactivity = TimeSpan.FromSeconds(19),
@@ -1068,6 +1068,11 @@
                             MetricsSubIntervalCount = 7,
                             SwapPath = Path.GetTempPath()
                         }
+                    },
+                    SystemDataRegionConfiguration = new SystemDataRegionConfiguration
+                    {
+                        InitialSize = 64 * 1024 * 1024,
+                        MaxSize = 128 * 1024 * 1024,
                     }
                 },
                 SslContextFactory = new SslContextFactory(),
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
index db287ce..5723534 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
@@ -74,6 +74,7 @@
             CheckDefaultProperties(new DataRegionConfiguration());
             CheckDefaultProperties(new ClientConnectorConfiguration());
             CheckDefaultProperties(new SqlConnectorConfiguration());
+            CheckDefaultProperties(new SystemDataRegionConfiguration());
         }
 
         /// <summary>
@@ -624,8 +625,6 @@
             Assert.AreEqual(DataStorageConfiguration.DefaultCheckpointWriteOrder, cfg.CheckpointWriteOrder);
             Assert.AreEqual(DataStorageConfiguration.DefaultWriteThrottlingEnabled, cfg.WriteThrottlingEnabled);
 
-            Assert.AreEqual(DataStorageConfiguration.DefaultSystemRegionInitialSize, cfg.SystemRegionInitialSize);
-            Assert.AreEqual(DataStorageConfiguration.DefaultSystemRegionMaxSize, cfg.SystemRegionMaxSize);
             Assert.AreEqual(DataStorageConfiguration.DefaultPageSize, cfg.PageSize);
             Assert.AreEqual(DataStorageConfiguration.DefaultConcurrencyLevel, cfg.ConcurrencyLevel);
             Assert.AreEqual(DataStorageConfiguration.DefaultWalAutoArchiveAfterInactivity,
@@ -637,6 +636,16 @@
         /// <summary>
         /// Checks the default properties.
         /// </summary>
+        /// <param name="cfg">System Data Region Config.</param>
+        private static void CheckDefaultProperties(SystemDataRegionConfiguration cfg)
+        {
+            Assert.AreEqual(SystemDataRegionConfiguration.DefaultInitialSize, cfg.InitialSize);
+            Assert.AreEqual(SystemDataRegionConfiguration.DefaultMaxSize, cfg.MaxSize);
+        }
+
+        /// <summary>
+        /// Checks the default properties.
+        /// </summary>
         /// <param name="cfg">Config.</param>
         private static void CheckDefaultProperties(DataRegionConfiguration cfg)
         {
@@ -886,8 +895,6 @@
                     MetricsRateTimeInterval = TimeSpan.FromSeconds(9),
                     CheckpointWriteOrder = Configuration.CheckpointWriteOrder.Random,
                     WriteThrottlingEnabled = true,
-                    SystemRegionInitialSize = 64 * 1024 * 1024,
-                    SystemRegionMaxSize = 128 * 1024 * 1024,
                     ConcurrencyLevel = 1,
                     PageSize = 8 * 1024,
                     WalAutoArchiveAfterInactivity = TimeSpan.FromMinutes(5),
@@ -924,6 +931,11 @@
                             MetricsSubIntervalCount = 7,
                             SwapPath = PathUtils.GetTempDirectoryName()
                         }
+                    },
+                    SystemDataRegionConfiguration = new SystemDataRegionConfiguration
+                    {
+                        InitialSize = 64 * 1024 * 1024,
+                        MaxSize = 128 * 1024 * 1024,
                     }
                 },
                 AuthenticationEnabled = false,
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
index a0dfef1..0cb7cc2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
@@ -114,7 +114,8 @@
                     DefaultDataRegionConfiguration = new DataRegionConfiguration
                     {
                         Name = "default"
-                    }
+                    },
+                    SystemDataRegionConfiguration = new SystemDataRegionConfiguration()
                 };
 
                 AssertExtensions.ReflectionEqual(dsCfg, resCfg.DataStorageConfiguration,
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/PlatformComputeBinarizable.cs
similarity index 73%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core.Tests/PlatformComputeBinarizable.cs
index e09e31b..0b75239 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/PlatformComputeBinarizable.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,21 +15,14 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Common
+namespace Apache.Ignite.Core.Tests
 {
     /// <summary>
-    /// Represents an Ignite platform.
+    /// Interop class.
     /// </summary>
-    internal enum PlatformType
+    public class PlatformComputeBinarizable
     {
-        /// <summary>
-        /// Java platform.
-        /// </summary>
-        Java = 0,
-
-        /// <summary>
-        /// .NET platform.
-        /// </summary>
-        DotNet = 1
+        /** */
+        public int Field { get; set; }
     }
 }
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 264ac93..1c52dfe 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs
@@ -48,6 +48,9 @@
             "org.apache.ignite.platform.PlatformServiceCallCollectionsThinTask";
 
         /** */
+        private readonly bool _useBinaryArray;
+
+        /** */
         protected IIgnite Grid1;
 
         /** */
@@ -56,6 +59,18 @@
         /** */
         protected IIgnite Grid3;
 
+        /** */
+        public CallPlatformServiceTest(bool useBinaryArray)
+        {
+            _useBinaryArray = useBinaryArray;
+        }
+
+        /** */
+        public CallPlatformServiceTest() : this(false)
+        {
+            // No-op.
+        }
+
         /// <summary>
         /// Start grids and deploy test service.
         /// </summary>
@@ -133,7 +148,8 @@
                     typeof(BinarizableTestValue))
                 {
                     NameMapper = BinaryBasicNameMapper.SimpleNameInstance
-                }
+                },
+                LifecycleHandlers = _useBinaryArray ? new[] { new SetUseBinaryArray() } : null
             };
         }
 
@@ -256,7 +272,9 @@
 
             public string contextAttribute(string name)
             {
-                return _ctx.CurrentCallContext.GetAttribute(name);
+                IServiceCallContext callCtx = _ctx.CurrentCallContext;
+
+                return callCtx == null ? null : callCtx.GetAttribute(name);
             }
 
             /** <inheritdoc /> */
@@ -342,4 +360,14 @@
             }
         }
     }
+
+    /// <summary> Tests with UseBinaryArray = true. </summary>
+    public class CallPlatformServiceTestBinaryArrays : CallPlatformServiceTest
+    {
+        /** */
+        public CallPlatformServiceTestBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
index 890b68d..12fd52d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
@@ -148,7 +148,7 @@
         int testParams(params object[] args);
 
         /** */
-        ServicesTest.PlatformComputeBinarizable testBinarizable(ServicesTest.PlatformComputeBinarizable x);
+        PlatformComputeBinarizable testBinarizable(PlatformComputeBinarizable x);
 
         /** */
         object[] testBinarizableArrayOfObjects(object[] x);
@@ -157,7 +157,7 @@
         IBinaryObject[] testBinaryObjectArray(IBinaryObject[] x);
 
         /** */
-        ServicesTest.PlatformComputeBinarizable[] testBinarizableArray(ServicesTest.PlatformComputeBinarizable[] x);
+        PlatformComputeBinarizable[] testBinarizableArray(PlatformComputeBinarizable[] x);
 
         /** */
         ICollection testBinarizableCollection(ICollection x);
@@ -179,7 +179,7 @@
 
         /** */
         Employee[] testEmployees(Employee[] emps);
-        
+
         /** */
         Account[] testAccounts();
 
@@ -208,6 +208,9 @@
         void testException(string exceptionClass);
 
         /** */
+        object testRoundtrip(object x);
+
+        /** */
         void sleep(long delayMs);
 
         /** */
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/JavaServiceDynamicProxy.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/JavaServiceDynamicProxy.cs
index 37dec5c..6988b63 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/JavaServiceDynamicProxy.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/JavaServiceDynamicProxy.cs
@@ -35,7 +35,7 @@
         public JavaServiceDynamicProxy(dynamic svc)
         {
             _svc = svc;
-        } 
+        }
 
         /** <inheritDoc /> */
         public bool isCancelled()
@@ -272,7 +272,7 @@
         }
 
         /** <inheritDoc /> */
-        public ServicesTest.PlatformComputeBinarizable testBinarizable(ServicesTest.PlatformComputeBinarizable x)
+        public PlatformComputeBinarizable testBinarizable(PlatformComputeBinarizable x)
         {
             return _svc.testBinarizable(x);
         }
@@ -290,7 +290,7 @@
         }
 
         /** <inheritDoc /> */
-        public ServicesTest.PlatformComputeBinarizable[] testBinarizableArray(ServicesTest.PlatformComputeBinarizable[] x)
+        public PlatformComputeBinarizable[] testBinarizableArray(PlatformComputeBinarizable[] x)
         {
             return _svc.testBinarizableArray(x);
         }
@@ -396,6 +396,12 @@
         }
 
         /** <inheritDoc /> */
+        public object testRoundtrip(object x)
+        {
+            return x;
+        }
+
+        /** <inheritDoc /> */
         public object contextAttribute(string name)
         {
             return _svc.contextAttribute(name);
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/Model.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/Model.cs
index 9c1a9fe..6e84726 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/Model.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/Model.cs
@@ -123,7 +123,15 @@
     // ReSharper disable once InconsistentNaming
     public enum ACL
     {
-        Allow, Deny
+        ALLOW, DENY
+    }
+    
+    /// <summary>
+    /// A enum is a clone of Java class AccessLevel with the same namespace.
+    /// </summary>
+    public enum AccessLevel
+    {
+        USER, SUPER
     }
 
     /// <summary>
@@ -132,6 +140,9 @@
     public class Role
     {
         public String Name { get; set; }
+
+        /** Tests declaration as System.Enum. */
+        public Enum AccessLevel { get; set; }
     }
 
     /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/PlatformTestService.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/PlatformTestService.cs
index 733cb28..53e95443 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/PlatformTestService.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/PlatformTestService.cs
@@ -365,9 +365,9 @@
         }
 
         /** <inheritDoc /> */
-        public ServicesTest.PlatformComputeBinarizable testBinarizable(ServicesTest.PlatformComputeBinarizable x)
+        public PlatformComputeBinarizable testBinarizable(PlatformComputeBinarizable x)
         {
-            return x == null ? null : new ServicesTest.PlatformComputeBinarizable { Field = x.Field + 1};
+            return x == null ? null : new PlatformComputeBinarizable { Field = x.Field + 1};
         }
 
         /** <inheritDoc /> */
@@ -379,8 +379,7 @@
             for (int i = 0; i < x.Length; i++)
                 x[i] = x[i] == null
                     ? null
-                    : new ServicesTest.PlatformComputeBinarizable
-                        {Field = ((ServicesTest.PlatformComputeBinarizable) x[i]).Field + 1};
+                    : new PlatformComputeBinarizable {Field = ((PlatformComputeBinarizable) x[i]).Field + 1};
 
             return x;
         }
@@ -398,10 +397,10 @@
         }
 
         /** <inheritDoc /> */
-        public ServicesTest.PlatformComputeBinarizable[] testBinarizableArray(ServicesTest.PlatformComputeBinarizable[] x)
+        public PlatformComputeBinarizable[] testBinarizableArray(PlatformComputeBinarizable[] x)
         {
             // ReSharper disable once CoVariantArrayConversion
-            return (ServicesTest.PlatformComputeBinarizable[])testBinarizableArrayOfObjects(x);
+            return (PlatformComputeBinarizable[])testBinarizableArrayOfObjects(x);
         }
 
         /** <inheritDoc /> */
@@ -414,8 +413,7 @@
 
             foreach (var x in arg)
             {
-                res.Add(new ServicesTest.PlatformComputeBinarizable
-                    {Field = ((ServicesTest.PlatformComputeBinarizable) x).Field + 1});
+                res.Add(new PlatformComputeBinarizable {Field = ((PlatformComputeBinarizable) x).Field + 1});
             }
 
             return res;
@@ -529,8 +527,8 @@
         public User[] testUsers()
         {
             return new[] {
-                new User {Id = 1, Acl = ACL.Allow, Role = new Role {Name = "admin"}},
-                new User {Id = 2, Acl = ACL.Deny, Role = new Role {Name = "user"}}
+                new User {Id = 1, Acl = ACL.ALLOW, Role = new Role {Name = "admin", AccessLevel = AccessLevel.SUPER}},
+                new User {Id = 2, Acl = ACL.DENY, Role = new Role {Name = "user", AccessLevel = AccessLevel.USER}}
             };
         }
 
@@ -623,6 +621,12 @@
         }
 
         /** <inheritDoc /> */
+        public object testRoundtrip(object x)
+        {
+            return x;
+        }
+
+        /** <inheritDoc /> */
         public object contextAttribute(string name)
         {
             return _context.CurrentCallContext.GetAttribute(name);
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServiceProxyTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServiceProxyTest.cs
index bc6ec70..72d961c 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServiceProxyTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServiceProxyTest.cs
@@ -24,9 +24,9 @@
     using System.Reflection;
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Impl.Binary;
-    using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.Impl.Memory;
     using Apache.Ignite.Core.Impl.Services;
+    using Apache.Ignite.Core.Platform;
     using Apache.Ignite.Core.Services;
     using NUnit.Framework;
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
index d393259..798a98e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
@@ -21,15 +21,19 @@
     using System.Collections.Generic;
     using System.IO;
     using System.Linq;
+    using System.Net;
     using System.Runtime.Serialization.Formatters.Binary;
     using System.Threading;
     using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Core.Client;
     using Apache.Ignite.Core.Cluster;
     using Apache.Ignite.Core.Common;
     using Apache.Ignite.Core.Impl;
     using Apache.Ignite.Core.Impl.Binary;
+    using Apache.Ignite.Core.Log;
     using Apache.Ignite.Core.Resource;
     using Apache.Ignite.Core.Services;
+    using Apache.Ignite.Core.Tests.Client.Cache;
     using Apache.Ignite.Core.Tests.Compute;
     using Apache.Ignite.Platform.Model;
     using NUnit.Framework;
@@ -43,6 +47,9 @@
         private const string SvcName = "Service1";
 
         /** */
+        private const string PlatformSvcName = "PlatformTestService";
+
+        /** */
         private const string CacheName = "cache1";
 
         /** */
@@ -61,8 +68,29 @@
         private IIgnite _client;
 
         /** */
+        private IIgniteClient _thinClient;
+
+        /** */
         protected IIgnite[] Grids;
 
+        /* Deploy Java service name. */
+        private string _javaSvcName;
+
+        /** */
+        private readonly bool _useBinaryArray;
+
+        /** */
+        public ServicesTest()
+        {
+            // No-op.
+        }
+
+        /** */
+        public ServicesTest(bool useBinaryArray)
+        {
+            _useBinaryArray = useBinaryArray;
+        }
+
         [TestFixtureTearDown]
         public void FixtureTearDown()
         {
@@ -76,6 +104,19 @@
         public void SetUp()
         {
             StartGrids();
+
+            _thinClient = Ignition.StartClient(GetClientConfiguration());
+
+            Services.DeployClusterSingleton(PlatformSvcName, new PlatformTestService());
+
+            _javaSvcName = TestUtils.DeployJavaService(Grid1);
+
+            // Verify descriptor
+            var descriptor = Services.GetServiceDescriptors().Single(x => x.Name == _javaSvcName);
+            Assert.AreEqual(_javaSvcName, descriptor.Name);
+
+            descriptor = _client.GetServices().GetServiceDescriptors().Single(x => x.Name == _javaSvcName);
+            Assert.AreEqual(_javaSvcName, descriptor.Name);
         }
 
         /// <summary>
@@ -279,6 +320,13 @@
 
             Services.DeployClusterSingleton(SvcName, svc);
 
+            // Check proxy creation with an invalid implementation.
+            Assert.Throws<ArgumentException>(() => 
+                Grid1.GetServices().GetServiceProxy<ITestIgniteService>(SvcName, false, new CustomServiceCallContext()));
+
+            Assert.Throws<ArgumentException>(() => 
+                Grid1.GetServices().GetDynamicServiceProxy(SvcName, false, new CustomServiceCallContext()));
+
             foreach (var grid in Grids)
             {
                 var nodeId = grid.GetCluster().ForLocal().GetNode().Id;
@@ -299,7 +347,7 @@
                     .Build();
 
                 var proxy = grid.GetServices().GetServiceProxy<ITestIgniteService>(SvcName, false, ctx);
-                
+
                 Assert.IsNull(proxy.ContextAttribute("not-exist-attribute"));
                 Assert.IsNull(proxy.ContextBinaryAttribute("not-exist-attribute"));
 
@@ -311,7 +359,7 @@
                 Assert.AreEqual(attrValue, stickyProxy.ContextAttribute(attrName));
                 Assert.AreEqual(attrValue, dynamicProxy.ContextAttribute(attrName));
                 Assert.AreEqual(attrValue, dynamicStickyProxy.ContextAttribute(attrName));
-                
+
                 Assert.AreEqual(attrBinValue, proxy.ContextBinaryAttribute(attrBinName));
                 Assert.AreEqual(attrBinValue, stickyProxy.ContextBinaryAttribute(attrBinName));
                 Assert.AreEqual(attrBinValue, dynamicProxy.ContextBinaryAttribute(attrBinName));
@@ -544,13 +592,15 @@
         [Test]
         public void TestServiceDescriptors()
         {
+            var beforeDeploy = Services.GetServiceDescriptors().Count;
+
             Services.DeployKeyAffinitySingleton(SvcName, new TestIgniteServiceSerializable(), CacheName, 1);
 
             var descriptors = Services.GetServiceDescriptors();
 
-            Assert.AreEqual(1, descriptors.Count);
+            Assert.AreEqual(beforeDeploy + 1, descriptors.Count);
 
-            var desc = descriptors.Single();
+            var desc = descriptors.First(d => d.Name == SvcName);
 
             Assert.AreEqual(SvcName, desc.Name);
             Assert.AreEqual(CacheName, desc.CacheName);
@@ -919,24 +969,11 @@
         [Test]
         public void TestCallJavaServiceRemote()
         {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(Grid1);
-
-            // Verify descriptor
-            var descriptor = _client.GetServices().GetServiceDescriptors().Single(x => x.Name == javaSvcName);
-            Assert.AreEqual(javaSvcName, descriptor.Name);
-
-            var svc = _client.GetServices().GetServiceProxy<IJavaService>(javaSvcName, false, callContext());
+            var svc = _client.GetServices().GetServiceProxy<IJavaService>(_javaSvcName, false, callContext());
             var binSvc = _client.GetServices().WithKeepBinary().WithServerKeepBinary()
-                .GetServiceProxy<IJavaService>(javaSvcName, false);
+                .GetServiceProxy<IJavaService>(_javaSvcName, false);
 
-            DoTestService(svc);
-
-            DoTestBinary(svc, binSvc);
-
-            DoTestJavaExceptions(svc, true);
-
-            Services.Cancel(javaSvcName);
+            DoAllServiceTests(svc, binSvc, true, false);
         }
 
         /// <summary>
@@ -945,40 +982,11 @@
         [Test]
         public void TestCallJavaServiceLocal()
         {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(Grid1);
-
-            // Verify descriptor
-            var descriptor = Services.GetServiceDescriptors().Single(x => x.Name == javaSvcName);
-            Assert.AreEqual(javaSvcName, descriptor.Name);
-
-            var svc = Services.GetServiceProxy<IJavaService>(javaSvcName, false, callContext());
+            var svc = Services.GetServiceProxy<IJavaService>(_javaSvcName, false, callContext());
             var binSvc = Services.WithKeepBinary().WithServerKeepBinary()
-                .GetServiceProxy<IJavaService>(javaSvcName, false);
+                .GetServiceProxy<IJavaService>(_javaSvcName, false);
 
-            DoTestService(svc);
-
-            DoTestBinary(svc, binSvc);
-
-            DoTestJavaExceptions(svc);
-
-            Services.Cancel(javaSvcName);
-        }
-
-        /// <summary>
-        /// Tests Java service invocation with dynamic proxy.
-        /// </summary>
-        [Test]
-        public void TestCallJavaServiceDynamicProxy()
-        {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(Grid1);
-
-            var svc = new JavaServiceDynamicProxy(Grid1.GetServices().GetDynamicServiceProxy(javaSvcName, true, callContext()));
-
-            DoTestService(svc);
-
-            DoTestJavaExceptions(svc);
+            DoAllServiceTests(svc, binSvc, false, false);
         }
 
         /// <summary>
@@ -987,7 +995,11 @@
         [Test]
         public void TestCallPlatformServiceRemote()
         {
-            DoTestPlatformService(_client.GetServices());
+            var svc = _client.GetServices().GetServiceProxy<IJavaService>(PlatformSvcName, false, callContext());
+            var binSvc = _client.GetServices().WithKeepBinary().WithServerKeepBinary()
+                .GetServiceProxy<IJavaService>(PlatformSvcName, false);
+
+            DoAllServiceTests(svc, binSvc, true, true);
         }
 
         /// <summary>
@@ -996,29 +1008,69 @@
         [Test]
         public void TestCallPlatformServiceLocal()
         {
-            DoTestPlatformService(Services);
+            var svc = Services.GetServiceProxy<IJavaService>(PlatformSvcName, false, callContext());
+            var binSvc = Services.WithKeepBinary().WithServerKeepBinary()
+                .GetServiceProxy<IJavaService>(PlatformSvcName, false);
+
+            DoAllServiceTests(svc, binSvc, false, true);
         }
 
         /// <summary>
-        /// Tests .Net service invocation.
+        /// Test .Net service invocation via thin client.
         /// </summary>
-        public void DoTestPlatformService(IServices svcsForProxy)
+        [Test]
+        public void TestCallPlatformServiceThinClient()
         {
-            const string platformSvcName = "PlatformTestService";
+            var svc = _thinClient.GetServices().GetServiceProxy<IJavaService>(PlatformSvcName);
+            var binSvc = _thinClient.GetServices().WithKeepBinary().WithServerKeepBinary()
+                .GetServiceProxy<IJavaService>(PlatformSvcName);
 
-            Services.DeployClusterSingleton(platformSvcName, new PlatformTestService());
+            DoAllServiceTests(svc, binSvc, false, true, false);
+        }
 
-            var svc = svcsForProxy.GetServiceProxy<IJavaService>(platformSvcName, false, callContext());
+        /// <summary>
+        /// Tests Java service invocation via thin client.
+        /// </summary>
+        [Test]
+        public void TestCallJavaServiceThinClient()
+        {
+            var svc = Services.GetServiceProxy<IJavaService>(_javaSvcName, false, callContext());
+            var binSvc = Services.WithKeepBinary().WithServerKeepBinary()
+                .GetServiceProxy<IJavaService>(_javaSvcName, false);
 
-            DoTestService(svc);
+            DoAllServiceTests(svc, binSvc, false, false, false);
+        }
 
-            Services.Cancel(platformSvcName);
+        /// <summary>
+        /// Tests Java service invocation with dynamic proxy.
+        /// </summary>
+        [Test]
+        public void TestCallJavaServiceDynamicProxy()
+        {
+            var svc = new JavaServiceDynamicProxy(Services.GetDynamicServiceProxy(_javaSvcName, true, callContext()));
+
+            DoTestService(svc, true);
+
+            DoTestJavaExceptions(svc);
+        }
+
+        /// <summary>
+        /// Tests service invocation.
+        /// </summary>
+        private void DoAllServiceTests(IJavaService svc, IJavaService binSvc, bool isClient, bool isPlatform, bool supportCtxAttrs = true)
+        {
+            DoTestService(svc, supportCtxAttrs);
+
+            DoTestBinary(svc, binSvc, isPlatform);
+
+            if (!isPlatform)
+                DoTestJavaExceptions(svc, isClient);
         }
 
         /// <summary>
         /// Tests service methods.
         /// </summary>
-        private void DoTestService(IJavaService svc)
+        private void DoTestService(IJavaService svc, bool supportCtxAttrs)
         {
             // Basics
             Assert.IsTrue(svc.isInitialized());
@@ -1146,7 +1198,8 @@
             Assert.AreEqual(dt1, cache.Get(3));
             Assert.AreEqual(dt2, cache.Get(4));
 
-            Assert.AreEqual("value", svc.contextAttribute("attr"));
+            if (supportCtxAttrs)
+                Assert.AreEqual("value", svc.contextAttribute("attr"));
 
 #if NETCOREAPP
             //This Date in Europe/Moscow have offset +4.
@@ -1236,14 +1289,14 @@
         /// <summary>
         /// Tets binary methods in services.
         /// </summary>
-        private void DoTestBinary(IJavaService svc, IJavaService binSvc)
+        private void DoTestBinary(IJavaService svc, IJavaService binSvc, bool isPlatform)
         {
             // Binary collections
             var arr = new[] {10, 11, 12}.Select(
                 x => new PlatformComputeBinarizable {Field = x}).ToArray();
             var arrOfObj = arr.ToArray<object>();
 
-            Assert.AreEqual(new[] {11, 12, 13}, svc.testBinarizableCollection(arr)
+            Assert.AreEqual(new[] {11, 12, 13}, svc.testBinarizableCollection(arr.ToList())
                 .OfType<PlatformComputeBinarizable>().Select(x => x.Field));
 
             var binarizableObjs = svc.testBinarizableArrayOfObjects(arrOfObj);
@@ -1256,7 +1309,9 @@
 
             var bins = svc.testBinarizableArray(arr);
 
-            Assert.AreEqual(typeof(PlatformComputeBinarizable[]), bins.GetType());
+            if (!isPlatform || _useBinaryArray)
+                Assert.AreEqual(typeof(PlatformComputeBinarizable[]), bins.GetType());
+
             Assert.AreEqual(new[] {11, 12, 13},bins.Select(x => x.Field));
 
             Assert.IsNull(svc.testBinarizableArray(null));
@@ -1360,18 +1415,41 @@
             return new IgniteConfiguration(TestUtils.GetTestConfiguration())
             {
                 SpringConfigUrl = springConfigUrl,
-                BinaryConfiguration = new BinaryConfiguration(
-                    typeof (TestIgniteServiceBinarizable),
-                    typeof (TestIgniteServiceBinarizableErr),
-                    typeof (PlatformComputeBinarizable),
-                    typeof (BinarizableObject))
-                {
-                    NameMapper = BinaryBasicNameMapper.SimpleNameInstance,
-                    ForceTimestamp = true
+                BinaryConfiguration = BinaryConfiguration(),
+                LifecycleHandlers = _useBinaryArray ? new[] { new SetUseBinaryArray() } : null
+            };
+        }
+
+        /// <summary>
+        /// Gets the client configuration.
+        /// </summary>
+        private IgniteClientConfiguration GetClientConfiguration()
+        {
+            var port = IgniteClientConfiguration.DefaultPort;
+
+            return new IgniteClientConfiguration
+            {
+                Endpoints = new List<string> {IPAddress.Loopback + ":" + port},
+                SocketTimeout = TimeSpan.FromSeconds(15),
+                Logger = new ListLogger(new ConsoleLogger {MinLevel = LogLevel.Trace}),
+                BinaryConfiguration = BinaryConfiguration()
+            };
+        }
+
+        /** */
+        private BinaryConfiguration BinaryConfiguration()
+        {
+            return new BinaryConfiguration(
+                typeof(TestIgniteServiceBinarizable),
+                typeof(TestIgniteServiceBinarizableErr),
+                typeof(PlatformComputeBinarizable),
+                typeof(BinarizableObject))
+            {
+                NameMapper = BinaryBasicNameMapper.SimpleNameInstance,
+                ForceTimestamp = true
 #if NETCOREAPP
-                    , TimestampConverter = new TimestampConverter()
+                , TimestampConverter = new TimestampConverter()
 #endif
-                }
             };
         }
 
@@ -1528,7 +1606,7 @@
 
             /** */
             object ContextAttribute(string name);
-            
+
             /** */
             object ContextBinaryAttribute(string name);
         }
@@ -1660,15 +1738,15 @@
             public object ContextAttribute(string name)
             {
                 IServiceCallContext ctx = _context.CurrentCallContext;
-                
+
                 return ctx == null ? null : ctx.GetAttribute(name);
             }
-            
+
             /** <inheritdoc /> */
             public object ContextBinaryAttribute(string name)
             {
                 IServiceCallContext ctx = _context.CurrentCallContext;
-                
+
                 return ctx == null ? null : ctx.GetBinaryAttribute(name);
             }
 
@@ -1821,15 +1899,6 @@
         }
 
         /// <summary>
-        /// Interop class.
-        /// </summary>
-        public class PlatformComputeBinarizable
-        {
-            /** */
-            public int Field { get; set; }
-        }
-
-        /// <summary>
         /// Class has no an equals class in Java.
         /// </summary>
         public class PlatformComputeBinarizable2
@@ -1852,6 +1921,24 @@
             }
         }
 
+        /// <summary>
+        /// Custom implementation of the service call context.
+        /// </summary>
+        private class CustomServiceCallContext : IServiceCallContext
+        {
+            /** <inheritdoc /> */
+            public string GetAttribute(string name)
+            {
+                return null;
+            }
+
+            /** <inheritdoc /> */
+            public byte[] GetBinaryAttribute(string name)
+            {
+                return null;
+            }
+        }
+
 #if NETCOREAPP
         /// <summary>
         /// Adds support of the local dates to the Ignite timestamp serialization.
@@ -1877,4 +1964,14 @@
         }
 #endif
     }
+
+    /// <summary> Tests with UseBinaryArray = true. </summary>
+    public class ServicesTestBinaryArrays : ServicesTest
+    {
+        /** */
+        public ServicesTestBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestAsync.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestAsync.cs
index 6fd6126..95472a8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestAsync.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestAsync.cs
@@ -32,5 +32,27 @@
         {
             get { return new ServicesAsyncWrapper(Grid1.GetServices()); }
         }
+
+        /** */
+        public ServicesTestAsync()
+        {
+            // No-op.
+        }
+
+        /** */
+        public ServicesTestAsync(bool useBinaryArray) : base(useBinaryArray)
+        {
+            // No-op.
+        }
+    }
+
+    /// <summary> Tests with UseBinaryArray = true. </summary>
+    public class ServicesTestAsyncBinaryArrays : ServicesTestAsync
+    {
+        /** */
+        public ServicesTestAsyncBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestFullFooter.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestFullFooter.cs
index 98c50cd..2c52299 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestFullFooter.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTestFullFooter.cs
@@ -33,5 +33,27 @@
         {
             get { return false; }
         }
+
+        /** */
+        public ServicesTestFullFooter()
+        {
+            // No-op.
+        }
+
+        /** */
+        public ServicesTestFullFooter(bool useBinaryArray) : base(useBinaryArray)
+        {
+            // No-op.
+        }
+    }
+
+    /// <summary> Tests with UseBinaryArray = true. </summary>
+    public class ServicesTestFullFooterBinaryArrays : ServicesTestFullFooter
+    {
+        /** */
+        public ServicesTestFullFooterBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTypeAutoResolveTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTypeAutoResolveTest.cs
index 09ce3c5..dd1c7ae 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTypeAutoResolveTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTypeAutoResolveTest.cs
@@ -22,8 +22,12 @@
     using System.Collections.Generic;
     using System.IO;
     using System.Linq;
+    using System.Net;
     using System.Reflection;
     using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Core.Client;
+    using Apache.Ignite.Core.Log;
+    using Apache.Ignite.Core.Tests.Client.Cache;
     using NUnit.Framework;
     using Apache.Ignite.Platform.Model;
 
@@ -33,6 +37,15 @@
     public class ServicesTypeAutoResolveTest
     {
         /** */
+        private readonly bool _useBinaryArray;
+
+        /** Platform service name. */
+        const string PlatformSvcName = "PlatformTestService";
+
+        /** Java service name. */
+        private string _javaSvcName;
+
+        /** */
         protected internal static readonly Employee[] Emps = new[]
         {
             new Employee {Fio = "Sarah Connor", Salary = 1},
@@ -54,6 +67,21 @@
         /** */
         private IIgnite _client;
 
+        /** */
+        private IIgniteClient _thinClient;
+
+        /** */
+        public ServicesTypeAutoResolveTest()
+        {
+            // No-op.
+        }
+
+        /** */
+        public ServicesTypeAutoResolveTest(bool useBinaryArray)
+        {
+            _useBinaryArray = useBinaryArray;
+        }
+
         [TestFixtureTearDown]
         public void FixtureTearDown()
         {
@@ -67,6 +95,9 @@
         public void SetUp()
         {
             StartGrids();
+
+            _grid1.GetServices().DeployClusterSingleton(PlatformSvcName, new PlatformTestService());
+            _javaSvcName = TestUtils.DeployJavaService(_grid1);
         }
 
         /// <summary>
@@ -77,6 +108,9 @@
         {
             try
             {
+                _grid1.GetServices().Cancel(PlatformSvcName);
+                _grid1.GetServices().Cancel(_javaSvcName);
+
                 _grid1.GetServices();
 
                 TestUtils.AssertHandleRegistryIsEmpty(1000, _grid1);
@@ -96,23 +130,21 @@
         }
 
         /// <summary>
-        /// Tests Java service invocation.
-        /// Types should be resolved implicitly.
+        /// Tests .Net service invocation on local node.
         /// </summary>
         [Test]
-        public void TestCallPlatformServiceLocal()
+        public void TestPlatformServiceLocal()
         {
-            const string platformSvcName = "PlatformTestService";
+            DoTestService(_grid1.GetServices().GetServiceProxy<IJavaService>(PlatformSvcName), true);
+        }
 
-            _grid1.GetServices().DeployClusterSingleton(platformSvcName, new PlatformTestService());
-
-            var svc = _grid1.GetServices().GetServiceProxy<IJavaService>(platformSvcName);
-
-            DoTestService(svc);
-
-            DoTestDepartments(svc);
-
-            _grid1.GetServices().Cancel(platformSvcName);
+        /// <summary>
+        /// Tests .Net service invocation on remote node.
+        /// </summary>
+        [Test]
+        public void TestPlatformServiceRemote()
+        {
+            DoTestService(_client.GetServices().GetServiceProxy<IJavaService>(PlatformSvcName), true);
         }
 
         /// <summary>
@@ -120,13 +152,9 @@
         /// Types should be resolved implicitly.
         /// </summary>
         [Test]
-        public void TestCallJavaServiceDynamicProxy()
+        public void TestJavaServiceDynamicProxy()
         {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(_grid1);
-            var svc = _grid1.GetServices().GetDynamicServiceProxy(javaSvcName, true);
-
-            DoTestService(new JavaServiceDynamicProxy(svc));
+            DoTestService(new JavaServiceDynamicProxy(_grid1.GetServices().GetDynamicServiceProxy(_javaSvcName, true)));
         }
 
         /// <summary>
@@ -134,18 +162,9 @@
         /// Types should be resolved implicitly.
         /// </summary>
         [Test]
-        public void TestCallJavaServiceLocal()
+        public void TestJavaServiceLocal()
         {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(_grid1);
-
-            var svc = _grid1.GetServices().GetServiceProxy<IJavaService>(javaSvcName, false);
-
-            DoTestService(svc);
-
-            DoTestDepartments(svc);
-
-            _grid1.GetServices().Cancel(javaSvcName);
+            DoTestService(_grid1.GetServices().GetServiceProxy<IJavaService>(_javaSvcName, false));
         }
 
         /// <summary>
@@ -153,57 +172,68 @@
         /// Types should be resolved implicitly.
         /// </summary>
         [Test]
-        public void TestCallJavaServiceRemote()
+        public void TestJavaServiceRemote()
         {
-            // Deploy Java service
-            var javaSvcName = TestUtils.DeployJavaService(_grid1);
-
-            var svc = _client.GetServices().GetServiceProxy<IJavaService>(javaSvcName, false);
-
-            DoTestService(svc);
-
-            DoTestDepartments(svc);
-
-            _grid1.GetServices().Cancel(javaSvcName);
+            DoTestService(_client.GetServices().GetServiceProxy<IJavaService>(_javaSvcName, false));
         }
 
         /// <summary>
-        /// Tests departments call.
+        /// Tests Java service invocation.
+        /// Types should be resolved implicitly.
         /// </summary>
-        private void DoTestDepartments(IJavaService svc)
+        [Test]
+        public void TestJavaServiceThinClient()
+        {
+            DoTestService(_thinClient.GetServices().GetServiceProxy<IJavaService>(_javaSvcName));
+        }
+
+        /// <summary>
+        /// Tests Platform service invocation.
+        /// Types should be resolved implicitly.
+        /// </summary>
+        [Test]
+        public void TestPlatformServiceThinClient()
+        {
+            DoTestService(_thinClient.GetServices().GetServiceProxy<IJavaService>(PlatformSvcName), true);
+        }
+
+        /// <summary>
+        /// Tests service invocation.
+        /// </summary>
+        private void DoTestService(IJavaService svc, bool isPlatform = false)
         {
             Assert.IsNull(svc.testDepartments(null));
 
-            var arr = new[] {"HR", "IT"}.Select(x => new Department() {Name = x}).ToArray();
+            var arr = new[] { "HR", "IT" }.Select(x => new Department() { Name = x }).ToList();
 
             ICollection deps = svc.testDepartments(arr);
 
             Assert.NotNull(deps);
             Assert.AreEqual(1, deps.Count);
             Assert.AreEqual("Executive", deps.OfType<Department>().Select(d => d.Name).ToArray()[0]);
-        }
 
-        /// <summary>
-        /// Tests java service instance.
-        /// </summary>
-        private static void DoTestService(IJavaService svc)
-        {
             Assert.IsNull(svc.testAddress(null));
 
-            Address addr = svc.testAddress(new Address {Zip = "000", Addr = "Moscow"});
+            Address addr = svc.testAddress(new Address { Zip = "000", Addr = "Moscow" });
 
             Assert.AreEqual("127000", addr.Zip);
             Assert.AreEqual("Moscow Akademika Koroleva 12", addr.Addr);
 
-            Assert.AreEqual(42, svc.testOverload(2, Emps));
-            Assert.AreEqual(43, svc.testOverload(2, Param));
-            Assert.AreEqual(3, svc.testOverload(1, 2));
-            Assert.AreEqual(5, svc.testOverload(3, 2));
+            if (_useBinaryArray)
+            {
+                Assert.AreEqual(42, svc.testOverload(2, Emps));
+                Assert.AreEqual(43, svc.testOverload(2, Param));
+                Assert.AreEqual(3, svc.testOverload(1, 2));
+                Assert.AreEqual(5, svc.testOverload(3, 2));
+            }
 
             Assert.IsNull(svc.testEmployees(null));
 
             var emps = svc.testEmployees(Emps);
 
+            if (!isPlatform || _useBinaryArray)
+                Assert.AreEqual(typeof(Employee[]), emps.GetType());
+
             Assert.NotNull(emps);
             Assert.AreEqual(1, emps.Length);
 
@@ -214,17 +244,20 @@
 
             var map = new Dictionary<Key, Value>();
 
-            map.Add(new Key() {Id = 1}, new Value() {Val = "value1"});
-            map.Add(new Key() {Id = 2}, new Value() {Val = "value2"});
+            map.Add(new Key() { Id = 1 }, new Value() { Val = "value1" });
+            map.Add(new Key() { Id = 2 }, new Value() { Val = "value2" });
 
             var res = svc.testMap(map);
 
             Assert.NotNull(res);
             Assert.AreEqual(1, res.Count);
-            Assert.AreEqual("value3", ((Value)res[new Key() {Id = 3}]).Val);
+            Assert.AreEqual("value3", ((Value)res[new Key() { Id = 3 }]).Val);
 
             var accs = svc.testAccounts();
 
+            if (!isPlatform || _useBinaryArray)
+                Assert.AreEqual(typeof(Account[]), accs.GetType());
+
             Assert.NotNull(accs);
             Assert.AreEqual(2, accs.Length);
             Assert.AreEqual("123", accs[0].Id);
@@ -234,14 +267,26 @@
 
             var users = svc.testUsers();
 
+            if (!isPlatform || _useBinaryArray)
+                Assert.AreEqual(typeof(User[]), users.GetType());
+
             Assert.NotNull(users);
             Assert.AreEqual(2, users.Length);
             Assert.AreEqual(1, users[0].Id);
-            Assert.AreEqual(ACL.Allow, users[0].Acl);
+            Assert.AreEqual(ACL.ALLOW, users[0].Acl);
             Assert.AreEqual("admin", users[0].Role.Name);
+            Assert.AreEqual(AccessLevel.SUPER, users[0].Role.AccessLevel);
             Assert.AreEqual(2, users[1].Id);
-            Assert.AreEqual(ACL.Deny, users[1].Acl);
+            Assert.AreEqual(ACL.DENY, users[1].Acl);
             Assert.AreEqual("user", users[1].Role.Name);
+            Assert.AreEqual(AccessLevel.USER, users[1].Role.AccessLevel);
+
+            var users2 = svc.testRoundtrip(users);
+
+            Assert.NotNull(users2);
+
+            if (_useBinaryArray)
+                Assert.AreEqual(typeof(User[]), users2.GetType());
         }
 
         /// <summary>
@@ -264,6 +309,7 @@
                 "client_work");
 
             _client = Ignition.Start(cfg);
+            _thinClient = Ignition.StartClient(GetClientConfiguration());
         }
 
         /// <summary>
@@ -286,11 +332,46 @@
             return new IgniteConfiguration(TestUtils.GetTestConfiguration())
             {
                 SpringConfigUrl = springConfigUrl,
-                BinaryConfiguration = new BinaryConfiguration
-                {
-                    NameMapper = new BinaryBasicNameMapper {NamespacePrefix = "org.", NamespaceToLower = true}
-                }
+                BinaryConfiguration = BinaryConfiguration(),
+                LifecycleHandlers = _useBinaryArray ? new[] { new SetUseBinaryArray() } : null
             };
         }
+
+        /// <summary>
+        /// Gets the client configuration.
+        /// </summary>
+        private IgniteClientConfiguration GetClientConfiguration()
+        {
+            var port = IgniteClientConfiguration.DefaultPort;
+
+            return new IgniteClientConfiguration
+            {
+                Endpoints = new List<string> {IPAddress.Loopback + ":" + port},
+                SocketTimeout = TimeSpan.FromSeconds(15),
+                Logger = new ListLogger(new ConsoleLogger {MinLevel = LogLevel.Trace}),
+                BinaryConfiguration = BinaryConfiguration()
+            };
+        }
+
+        /** */
+        private BinaryConfiguration BinaryConfiguration()
+        {
+            return new BinaryConfiguration
+            {
+                NameMapper = new BinaryBasicNameMapper { NamespacePrefix = "org.", NamespaceToLower = true }
+            };
+        }
+    }
+
+    /// <summary>
+    /// Tests checks ability to execute service method without explicit registration of parameter type.
+    /// </summary>
+    public class ServicesTypeAutoResolveTestBinaryArrays : ServicesTypeAutoResolveTest
+    {
+        /** */
+        public ServicesTypeAutoResolveTestBinaryArrays() : base(true)
+        {
+            // No-op.
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/TestUtils.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/TestUtils.cs
index 44a7012..2ab0594 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/TestUtils.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/TestUtils.cs
@@ -41,7 +41,9 @@
     using Apache.Ignite.Core.Impl.Client;
     using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.Impl.Unmanaged.Jni;
+    using Apache.Ignite.Core.Lifecycle;
     using Apache.Ignite.Core.Log;
+    using Apache.Ignite.Core.Resource;
     using Apache.Ignite.Core.Tests.Process;
     using NUnit.Framework;
     using NUnit.Framework.Interfaces;
@@ -442,19 +444,18 @@
         /// <param name="timeout">Timeout, in milliseconds.</param>
         public static void AssertHandleRegistryHasItems(IIgnite grid, int expectedCount, int timeout)
         {
-            var handleRegistry = ((Ignite)grid).HandleRegistry;
+            Func<IEnumerable<KeyValuePair<long, object>>> getItems = () =>
+                ((Ignite)grid).HandleRegistry.GetItems().Where(x => !(x.Value is LifecycleHandlerHolder));
 
-            expectedCount++;  // Skip default lifecycle bean
-
-            if (WaitForCondition(() => handleRegistry.Count == expectedCount, timeout))
+            if (WaitForCondition(() => getItems().Count() == expectedCount, timeout))
                 return;
 
-            var items = handleRegistry.GetItems().Where(x => !(x.Value is LifecycleHandlerHolder)).ToList();
+            var items = getItems().ToList();
 
             if (items.Any())
             {
                 Assert.Fail("HandleRegistry is not empty in grid '{0}' (expected {1}, actual {2}):\n '{3}'",
-                    grid.Name, expectedCount, handleRegistry.Count,
+                    grid.Name, expectedCount, items.Count,
                     items.Select(x => x.ToString()).Aggregate((x, y) => x + "\n" + y));
             }
         }
@@ -730,4 +731,25 @@
             }
         }
     }
+
+    /** */
+    public  class SetUseBinaryArray : ILifecycleHandler
+    {
+        /** Task name. */
+        private const string SetUseTypedArrayTask = "org.apache.ignite.platform.PlatformSetUseBinaryArrayTask";
+
+        /** */
+        [InstanceResource]
+        private readonly IIgnite _ignite = null;
+
+        /** <inheritdoc /> */
+        public void OnLifecycleEvent(LifecycleEventType evt)
+        {
+            if (evt != LifecycleEventType.AfterNodeStart && evt != LifecycleEventType.BeforeNodeStop)
+                return;
+
+            _ignite.GetCompute()
+                .ExecuteJavaTask<object>(SetUseTypedArrayTask, evt == LifecycleEventType.AfterNodeStart);
+        }
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientOperationType.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientOperationType.cs
new file mode 100644
index 0000000..51aeb70
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientOperationType.cs
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Client
+{
+    using Apache.Ignite.Core.Client.Cache;
+    using Apache.Ignite.Core.Client.Compute;
+    using Apache.Ignite.Core.Client.Services;
+    using Apache.Ignite.Core.Client.Transactions;
+
+    /// <summary>
+    /// Client operation type.
+    /// </summary>
+    public enum ClientOperationType
+    {
+        /// <summary>
+        /// Create cache <see cref="IIgniteClient.CreateCache{TK,TV}(string)"/>,
+        /// <see cref="IIgniteClient.CreateCache{TK,TV}(CacheClientConfiguration)"/>.
+        /// </summary>
+        CacheCreate,
+
+        /// <summary>
+        /// Get or create cache <see cref="IIgniteClient.GetOrCreateCache{TK,TV}(string)"/>,
+        /// <see cref="IIgniteClient.GetOrCreateCache{TK,TV}(CacheClientConfiguration)"/>.
+        /// </summary>
+        CacheGetOrCreate,
+
+        /// <summary>
+        /// Get cache names <see cref="IIgniteClient.GetCacheNames"/>.
+        /// </summary>
+        CacheGetNames,
+
+        /// <summary>
+        /// Destroy cache <see cref="IIgniteClient.DestroyCache"/>.
+        /// </summary>
+        CacheDestroy,
+
+        /// <summary>
+        /// Get value from cache <see cref="ICacheClient{TK,TV}.Get"/>.
+        /// </summary>
+        CacheGet,
+
+        /// <summary>
+        /// Put value to cache <see cref="ICacheClient{TK,TV}.Put"/>.
+        /// </summary>
+        CachePut,
+
+        /// <summary>
+        /// Determines if the cache contains a key <see cref="ICacheClient{TK,TV}.Put"/>.
+        /// </summary>
+        CacheContainsKey,
+
+        /// <summary>
+        /// Determines if the cache contains multiple keys (<see cref="ICacheClient{TK,TV}.ContainsKeys"/>).
+        /// </summary>
+        CacheContainsKeys,
+
+        /// <summary>
+        /// Get cache configuration (<see cref="ICacheClient{TK,TV}.GetConfiguration"/>).
+        /// </summary>
+        CacheGetConfiguration,
+
+        /// <summary>
+        /// Get cache size (<see cref="ICacheClient{TK,TV}.GetSize"/>).
+        /// </summary>
+        CacheGetSize,
+
+        /// <summary>
+        /// Put values to cache (<see cref="ICacheClient{TK,TV}.PutAll"/>).
+        /// </summary>
+        CachePutAll,
+
+        /// <summary>
+        /// Get values from cache (<see cref="ICacheClient{TK,TV}.GetAll"/>).
+        /// </summary>
+        CacheGetAll,
+
+        /// <summary>
+        /// Replace cache value (<see cref="ICacheClient{TK,TV}.Replace(TK,TV)"/>,
+        /// <see cref="ICacheClient{TK,TV}.Replace(TK,TV,TV)"/>).
+        /// </summary>
+        CacheReplace,
+
+        /// <summary>
+        /// Remove entry from cache (<see cref="ICacheClient{TK,TV}.Remove(TK)" />,
+        /// <see cref="ICacheClient{TK,TV}.Remove(TK,TV)"/>).
+        /// </summary>
+        CacheRemoveOne,
+
+        /// <summary>
+        /// Remove entries from cache (<see cref="ICacheClient{TK,TV}.RemoveAll(System.Collections.Generic.IEnumerable{TK})"/>).
+        /// </summary>
+        CacheRemoveMultiple,
+
+        /// <summary>
+        /// Remove everything from cache (<see cref="ICacheClient{TK,TV}.RemoveAll()"/>).
+        /// </summary>
+        CacheRemoveEverything,
+
+        /// <summary>
+        /// Clear cache entry (<see cref="ICacheClient{TK,TV}.Clear(TK)"/>).
+        /// </summary>
+        CacheClearOne,
+
+        /// <summary>
+        /// Clear multiple cache entries (<see cref="ICacheClient{TK,TV}.ClearAll"/>).
+        /// </summary>
+        CacheClearMultiple,
+
+        /// <summary>
+        /// Clear entire cache (<see cref="ICacheClient{TK,TV}.Clear()"/>).
+        /// </summary>
+        CacheClearEverything,
+
+        /// <summary>
+        /// Get and put (<see cref="ICacheClient{TK,TV}.GetAndPut(TK, TV)"/>).
+        /// </summary>
+        CacheGetAndPut,
+
+        /// <summary>
+        /// Get and remove (<see cref="ICacheClient{TK,TV}.GetAndRemove(TK)"/>).
+        /// </summary>
+        CacheGetAndRemove,
+
+        /// <summary>
+        /// Get and replace (<see cref="ICacheClient{TK,TV}.GetAndReplace(TK, TV)"/>).
+        /// </summary>
+        CacheGetAndReplace,
+
+        /// <summary>
+        /// Put if absent (<see cref="ICacheClient{TK,TV}.PutIfAbsent(TK, TV)"/>).
+        /// </summary>
+        CachePutIfAbsent,
+
+        /// <summary>
+        /// Get and put if absent (<see cref="ICacheClient{TK,TV}.GetAndPutIfAbsent(TK, TV)"/>).
+        /// </summary>
+        CacheGetAndPutIfAbsent,
+
+        /// <summary>
+        /// Scan query (<see cref="ICacheClient{TK,TV}.Query(Apache.Ignite.Core.Cache.Query.ScanQuery{TK,TV})"/>).
+        /// </summary>
+        QueryScan,
+
+        /// <summary>
+        /// SQL query (<see cref="ICacheClient{TK,TV}.Query(Apache.Ignite.Core.Cache.Query.SqlFieldsQuery)"/>).
+        /// </summary>
+        QuerySql,
+
+        /// <summary>
+        /// Continuous query (<see cref="ICacheClient{TK,TV}.QueryContinuous"/>).
+        /// </summary>
+        QueryContinuous,
+
+        /// <summary>
+        /// Start transaction (<see cref="ITransactionsClient.TxStart()"/>).
+        /// </summary>
+        TransactionStart,
+
+        /// <summary>
+        /// Get cluster state (<see cref="IClientCluster.IsActive"/>).
+        /// </summary>
+        ClusterGetState,
+
+        /// <summary>
+        /// Change cluster state (<see cref="IClientCluster.SetActive"/>).
+        /// </summary>
+        ClusterChangeState,
+
+        /// <summary>
+        /// Get cluster WAL state (<see cref="IClientCluster.IsWalEnabled"/>).
+        /// </summary>
+        ClusterGetWalState,
+
+        /// <summary>
+        /// Change cluster WAL state (<see cref="IClientCluster.EnableWal"/>, <see cref="IClientCluster.DisableWal"/>).
+        /// </summary>
+        ClusterChangeWalState,
+
+        /// <summary>
+        /// Get cluster nodes (<see cref="IClientClusterGroup.GetNodes"/>).
+        /// </summary>
+        ClusterGroupGetNodes,
+
+        /// <summary>
+        /// Execute compute task (<see cref="IComputeClient.ExecuteJavaTask{TRes}"/>).
+        /// </summary>
+        ComputeTaskExecute,
+
+        /// <summary>
+        /// Invoke service.
+        /// </summary>
+        ServiceInvoke,
+
+        /// <summary>
+        /// Get service descriptors (<see cref="IServicesClient.GetServiceDescriptors"/>).
+        /// </summary>
+        ServiceGetDescriptors,
+
+        /// <summary>
+        /// Get service descriptor (<see cref="IServicesClient.GetServiceDescriptor"/>).
+        /// </summary>
+        ServiceGetDescriptor
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryAllPolicy.cs
similarity index 73%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryAllPolicy.cs
index e09e31b..78c901c 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryAllPolicy.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,21 +15,17 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Common
+namespace Apache.Ignite.Core.Client
 {
     /// <summary>
-    /// Represents an Ignite platform.
+    /// Retry policy that always returns <c>true</c>.
     /// </summary>
-    internal enum PlatformType
+    public class ClientRetryAllPolicy : IClientRetryPolicy
     {
-        /// <summary>
-        /// Java platform.
-        /// </summary>
-        Java = 0,
-
-        /// <summary>
-        /// .NET platform.
-        /// </summary>
-        DotNet = 1
+        /** <inheritDoc /> */
+        public bool ShouldRetry(IClientRetryPolicyContext context)
+        {
+            return true;
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryReadPolicy.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryReadPolicy.cs
new file mode 100644
index 0000000..63834ac
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientRetryReadPolicy.cs
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Client
+{
+    using Apache.Ignite.Core.Impl.Common;
+
+    /// <summary>
+    /// Retry policy that returns true for all read-only operations that do not modify data.
+    /// </summary>
+    public sealed class ClientRetryReadPolicy : IClientRetryPolicy
+    {
+        /** <inheritDoc /> */
+        public bool ShouldRetry(IClientRetryPolicyContext context)
+        {
+            IgniteArgumentCheck.NotNull(context, nameof(context));
+
+            switch (context.Operation)
+            {
+                case ClientOperationType.CacheGetNames:
+                case ClientOperationType.CacheGet:
+                case ClientOperationType.CacheContainsKey:
+                case ClientOperationType.CacheContainsKeys:
+                case ClientOperationType.CacheGetConfiguration:
+                case ClientOperationType.CacheGetSize:
+                case ClientOperationType.CacheGetAll:
+                case ClientOperationType.QueryScan:
+                case ClientOperationType.QueryContinuous:
+                case ClientOperationType.ClusterGetState:
+                case ClientOperationType.ClusterGetWalState:
+                case ClientOperationType.ClusterGroupGetNodes:
+                case ClientOperationType.ServiceGetDescriptors:
+                case ClientOperationType.ServiceGetDescriptor:
+                    return true;
+
+                default:
+                    return false;
+            }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicy.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicy.cs
new file mode 100644
index 0000000..a4e2fd0
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicy.cs
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Client
+{
+    /// <summary>
+    /// Client retry policy determines whether client operations that have failed due to a connection issue
+    /// should be retried.
+    /// </summary>
+    public interface IClientRetryPolicy
+    {
+        /// <summary>
+        /// Gets a value indicating whether a client operation that has failed due to a connection issue
+        /// should be retried.
+        /// </summary>
+        /// <param name="context">Operation context.</param>
+        /// <returns>
+        /// <c>true</c> if the operation should be retried on another connection, <c>false</c> otherwise.
+        /// </returns>
+        bool ShouldRetry(IClientRetryPolicyContext context);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicyContext.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicyContext.cs
new file mode 100644
index 0000000..692ef1c
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientRetryPolicyContext.cs
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Client
+{
+    using System;
+
+    /// <summary>
+    /// Retry policy context. See <see cref="IClientRetryPolicy.ShouldRetry"/>.
+    /// </summary>
+    public interface IClientRetryPolicyContext
+    {
+        /// <summary>
+        /// Gets the client configuration.
+        /// </summary>
+        IgniteClientConfiguration Configuration { get; }
+
+        /// <summary>
+        /// Gets the operation type.
+        /// </summary>
+        ClientOperationType Operation { get; }
+
+        /// <summary>
+        /// Gets the current iteration.
+        /// </summary>
+        int Iteration { get; }
+
+        /// <summary>
+        /// Gets the exception that caused current retry iteration.
+        /// </summary>
+        Exception Exception { get; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs
index cb38b72..3692f2f7 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs
@@ -131,6 +131,9 @@
             {
                 TransactionConfiguration = new TransactionClientConfiguration(cfg.TransactionConfiguration);
             }
+
+            RetryLimit = cfg.RetryLimit;
+            RetryPolicy = cfg.RetryPolicy;
         }
 
         /// <summary>
@@ -245,6 +248,25 @@
         public TransactionClientConfiguration TransactionConfiguration { get; set; }
 
         /// <summary>
+        /// Gets or sets the retry policy. When a request fails due to a connection error,
+        /// Ignite will retry the request if the specified policy allows it.
+        /// <para />
+        /// Default is null: operations won't be retried.
+        /// <para />
+        /// See also <see cref="ClientRetryAllPolicy"/>, <see cref="ClientRetryReadPolicy"/>, <see cref="RetryLimit"/>.
+        /// </summary>
+        public IClientRetryPolicy RetryPolicy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the retry limit. When a request fails due to a connection error,
+        /// Ignite will retry the request if the specified <see cref="RetryPolicy"/> allows it. When this property is
+        /// greater than <c>0</c>, Ignite will limit the number of retries.
+        /// <para />
+        /// Default is <c>0</c>: no limit on retries.
+        /// </summary>
+        public int RetryLimit { get; set; }
+
+        /// <summary>
         /// Gets or sets custom binary processor. Internal property for tests.
         /// </summary>
         internal IBinaryProcessor BinaryProcessor { get; set; }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IClientServiceDescriptor.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IClientServiceDescriptor.cs
new file mode 100644
index 0000000..82e0a37
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IClientServiceDescriptor.cs
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Client.Services
+{
+    using System;
+    using Apache.Ignite.Core.Platform;
+
+    /// <summary>
+    /// Descriptor of Service.
+    /// </summary>
+    public interface IClientServiceDescriptor
+    {
+        /// <summary>
+        /// Gets service name.
+        /// </summary>
+        /// <returns>Service name.</returns>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets service class name.
+        /// </summary>
+        /// <returns>Service class name.</returns>
+        string ServiceClass { get; }
+
+        /// <summary>
+        /// Gets maximum allowed total number of deployed services in the grid, 0 for unlimited.
+        /// </summary>
+        /// <returns>Maximum allowed total number of deployed services in the grid, 0 for unlimited.</returns>
+        int TotalCount { get; }
+
+        /// <summary>
+        /// Gets maximum allowed number of deployed services on each node, {@code 0} for unlimited.
+        /// </summary>
+        /// <returns>Maximum allowed total number of deployed services on each node, {@code 0} for unlimited.</returns>
+        int MaxPerNodeCount { get; }
+
+        /// <summary>
+        /// Gets cache name used for key-to-node affinity calculation.
+        /// This parameter is optional and is set only when key-affinity service was deployed.
+        /// </summary>
+        /// <returns>Cache name, possibly null.</returns>
+        string CacheName { get; }
+
+        /// <summary>
+        /// Gets ID of grid node that initiated the service deployment.
+        /// </summary>
+        /// <returns>ID of grid node that initiated the service deployment.</returns>
+        Guid? OriginNodeId { get; }
+
+        /// <summary>
+        /// Platform type.
+        /// </summary>
+        /// <returns>Platform type.</returns>
+        PlatformType PlatformType { get; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IServicesClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IServicesClient.cs
index 49c64fd..f788d67 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IServicesClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Services/IServicesClient.cs
@@ -17,6 +17,10 @@
 
 namespace Apache.Ignite.Core.Client.Services
 {
+    using System.Collections.Generic;
+    using Apache.Ignite.Core.Common;
+    using Apache.Ignite.Core.Services;
+
     /// <summary>
     /// Ignite distributed services client.
     /// </summary>
@@ -39,6 +43,32 @@
         T GetServiceProxy<T>(string serviceName) where T : class;
 
         /// <summary>
+        /// Gets a proxy for the service with the specified name and caller context.
+        /// <para />
+        /// Note: service proxies are not "sticky" - there is no guarantee that all calls will be made to the same
+        /// remote service instance.
+        /// </summary>
+        /// <typeparam name="T">Service type.</typeparam>
+        /// <param name="serviceName">Service name.</param>
+        /// <param name="callCtx">Service call context.</param>
+        /// <returns>Proxy object that forwards all member calls to a remote Ignite service.</returns>
+        [IgniteExperimental]
+        T GetServiceProxy<T>(string serviceName, IServiceCallContext callCtx) where T : class;
+
+        /// <summary>
+        /// Gets metadata about all deployed services in the grid.
+        /// </summary>
+        /// <returns>Metadata about all deployed services in the grid.</returns>
+        ICollection<IClientServiceDescriptor> GetServiceDescriptors();
+
+        /// <summary>
+        /// Gets metadata about service deployed in the grid.
+        /// </summary>
+        /// <param name="serviceName">Service name.</param>
+        /// <returns>Metadata about all deployed services in the grid.</returns>
+        IClientServiceDescriptor GetServiceDescriptor(string serviceName);
+
+        /// <summary>
         /// Returns an instance with binary mode enabled.
         /// Service method results will be kept in binary form.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataStorageConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataStorageConfiguration.cs
index 671fd29..b8cfe09 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataStorageConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataStorageConfiguration.cs
@@ -146,13 +146,15 @@
         /// <summary>
         /// Default size of a memory chunk reserved for system cache initially.
         /// </summary>
+        [Obsolete("Use SystemDataRegionConfiguration.DefaultInitialSize")]
         public const long DefaultSystemRegionInitialSize = 40 * 1024 * 1024;
-
+        
         /// <summary>
         /// Default max size of a memory chunk for the system cache.
         /// </summary>
+        [Obsolete("Use SystemDataRegionConfiguration.DefaultMaxSize")]
         public const long DefaultSystemRegionMaxSize = 100 * 1024 * 1024;
-
+        
         /// <summary>
         /// The default page size.
         /// </summary>
@@ -196,8 +198,6 @@
             CheckpointWriteOrder = DefaultCheckpointWriteOrder;
             WriteThrottlingEnabled = DefaultWriteThrottlingEnabled;
             WalCompactionEnabled = DefaultWalCompactionEnabled;
-            SystemRegionInitialSize = DefaultSystemRegionInitialSize;
-            SystemRegionMaxSize = DefaultSystemRegionMaxSize;
             PageSize = DefaultPageSize;
             WalAutoArchiveAfterInactivity = DefaultWalAutoArchiveAfterInactivity;
             WalForceArchiveTimeout = DefaultWalAutoArchiveAfterInactivity;
@@ -237,8 +237,10 @@
             WalCompactionEnabled = reader.ReadBoolean();
             MaxWalArchiveSize = reader.ReadLong();
 
+#pragma warning disable 618
             SystemRegionInitialSize = reader.ReadLong();
             SystemRegionMaxSize = reader.ReadLong();
+#pragma warning restore 618
             PageSize = reader.ReadInt();
             ConcurrencyLevel = reader.ReadInt();
             WalAutoArchiveAfterInactivity = reader.ReadLongAsTimespan();
@@ -260,6 +262,11 @@
             {
                 DefaultDataRegionConfiguration = new DataRegionConfiguration(reader);
             }
+
+            if (reader.ReadBoolean())
+            {
+                SystemDataRegionConfiguration = new SystemDataRegionConfiguration(reader);
+            }
         }
 
         /// <summary>
@@ -293,8 +300,10 @@
             writer.WriteBoolean(WalCompactionEnabled);
             writer.WriteLong(MaxWalArchiveSize);
 
+#pragma warning disable 618
             writer.WriteLong(SystemRegionInitialSize);
             writer.WriteLong(SystemRegionMaxSize);
+#pragma warning restore 618
             writer.WriteInt(PageSize);
             writer.WriteInt(ConcurrencyLevel);
             writer.WriteTimeSpanAsLong(WalAutoArchiveAfterInactivity);
@@ -332,6 +341,16 @@
             {
                 writer.WriteBoolean(false);
             }
+
+            if (SystemDataRegionConfiguration != null)
+            {
+                writer.WriteBoolean(true);
+                SystemDataRegionConfiguration.Write(writer);
+            }
+            else
+            {
+                writer.WriteBoolean(false);
+            }
         }
 
         /// <summary>
@@ -477,14 +496,40 @@
         /// <summary>
         /// Gets or sets the size of a memory chunk reserved for system needs.
         /// </summary>
-        [DefaultValue(DefaultSystemRegionInitialSize)]
-        public long SystemRegionInitialSize { get; set; }
+        [DefaultValue(SystemDataRegionConfiguration.DefaultInitialSize)]
+        [Obsolete("Use SystemDataRegionConfiguration.InitialSize")]
+        public long SystemRegionInitialSize
+        {
+            get => SystemDataRegionConfiguration?.InitialSize ?? SystemDataRegionConfiguration.DefaultInitialSize;
+            set
+            {
+                if (SystemDataRegionConfiguration == null)
+                {
+                    SystemDataRegionConfiguration = new SystemDataRegionConfiguration();
+                }
+
+                SystemDataRegionConfiguration.InitialSize = value;
+            }
+        }
 
         /// <summary>
         /// Gets or sets the maximum memory region size reserved for system needs.
         /// </summary>
-        [DefaultValue(DefaultSystemRegionMaxSize)]
-        public long SystemRegionMaxSize { get; set; }
+        [DefaultValue(SystemDataRegionConfiguration.DefaultMaxSize)]
+        [Obsolete("Use SystemDataRegionConfiguration.MaxSize")]
+        public long SystemRegionMaxSize
+        {
+            get => SystemDataRegionConfiguration?.MaxSize ?? SystemDataRegionConfiguration.DefaultMaxSize;
+            set
+            {
+                if (SystemDataRegionConfiguration == null)
+                {
+                    SystemDataRegionConfiguration = new SystemDataRegionConfiguration();
+                }
+
+                SystemDataRegionConfiguration.MaxSize = value;
+            }
+        }
 
         /// <summary>
         /// Gets or sets the size of the memory page.
@@ -534,5 +579,10 @@
         /// Gets or sets the default region configuration.
         /// </summary>
         public DataRegionConfiguration DefaultDataRegionConfiguration { get; set; }
+
+        /// <summary>
+        /// Gets or sets the system region configuration.
+        /// </summary>
+        public SystemDataRegionConfiguration SystemDataRegionConfiguration { get; set; }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/SystemDataRegionConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/SystemDataRegionConfiguration.cs
new file mode 100644
index 0000000..6f7338a
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/SystemDataRegionConfiguration.cs
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Configuration
+{
+    using System.ComponentModel;
+    using System.Diagnostics;
+    using Apache.Ignite.Core.Binary;
+
+    /// <summary>
+    /// Data storage configuration for system cache.
+    /// </summary>
+    public class SystemDataRegionConfiguration
+    {
+        /// <summary>
+        /// Default size in bytes of a memory chunk reserved for system cache initially.
+        /// </summary>
+        public const long DefaultInitialSize = 40 * 1024 * 1024;
+
+        /// <summary>
+        /// Default max size in bytes of a memory chunk for the system cache.
+        /// </summary>
+        public const long DefaultMaxSize = 100 * 1024 * 1024;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SystemDataRegionConfiguration"/> class.
+        /// </summary>
+        public SystemDataRegionConfiguration()
+        {
+            InitialSize = DefaultInitialSize;
+            MaxSize = DefaultMaxSize;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DataStorageConfiguration"/> class.
+        /// </summary>
+        /// <param name="reader">The reader.</param>
+        internal SystemDataRegionConfiguration(IBinaryRawReader reader)
+        {
+            Debug.Assert(reader != null);
+
+            InitialSize = reader.ReadLong();
+            MaxSize = reader.ReadLong();
+        }
+        
+        /// <summary>
+        /// Writes this instance to the specified writer.
+        /// </summary>
+        /// <param name="writer">The writer.</param>
+        internal void Write(IBinaryRawWriter writer)
+        {
+           Debug.Assert(writer != null);
+           
+           writer.WriteLong(InitialSize);
+           writer.WriteLong(MaxSize);
+        }
+        
+        /// <summary>
+        /// Gets or sets the size in bytes of a memory chunk reserved for system needs.
+        /// </summary>
+        [DefaultValue(DefaultInitialSize)]
+        public long InitialSize { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the maximum memory region size in bytes reserved for system needs.
+        /// </summary>
+        [DefaultValue(DefaultMaxSize)]
+        public long MaxSize { get; set; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd
index b610318..4c3e26f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd
@@ -282,6 +282,18 @@
                         </xs:attribute>
                     </xs:complexType>
                 </xs:element>
+                <xs:element name="retryPolicy" minOccurs="0">
+                    <xs:annotation>
+                        <xs:documentation>Retry policy for failed operations.</xs:documentation>
+                    </xs:annotation>
+                    <xs:complexType>
+                        <xs:attribute name="type" type="xs:string" use="required">
+                            <xs:annotation>
+                                <xs:documentation>Assembly-qualified type name.</xs:documentation>
+                            </xs:annotation>
+                        </xs:attribute>
+                    </xs:complexType>
+                </xs:element>
             </xs:all>
             <xs:attribute name="host" type="xs:string" use="required">
                 <xs:annotation>
@@ -333,6 +345,11 @@
                     <xs:documentation>Password to be used to connect to secured cluster.</xs:documentation>
                 </xs:annotation>
             </xs:attribute>
+            <xs:attribute name="retryLimit" type="xs:int">
+                <xs:annotation>
+                    <xs:documentation>Operation retry limit when RetryPolicy is set.</xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
         </xs:complexType>
     </xs:element>
 </xs:schema>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
index 462949c..e2cbdcb 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
@@ -1924,6 +1924,23 @@
                                     </xs:sequence>
                                 </xs:complexType>
                             </xs:element>
+                            <xs:element name="systemDataRegionConfiguration">
+                                <xs:annotation>
+                                    <xs:documentation>System data region configuration.</xs:documentation>
+                                </xs:annotation>
+                                <xs:complexType>
+                                    <xs:attribute name="initialSize" type="xs:long">
+                                        <xs:annotation>
+                                            <xs:documentation>Initial size of system data region.</xs:documentation>
+                                        </xs:annotation>
+                                    </xs:attribute>
+                                    <xs:attribute name="maxSize" type="xs:long">
+                                        <xs:annotation>
+                                            <xs:documentation>Maximum size of system data region.</xs:documentation>
+                                        </xs:annotation>
+                                    </xs:attribute>
+                                </xs:complexType>
+                            </xs:element>
                         </xs:all>
                         <xs:attribute name="storagePath" type="xs:string">
                             <xs:annotation>
@@ -2042,12 +2059,12 @@
                         </xs:attribute>
                         <xs:attribute name="systemRegionInitialSize" type="xs:int">
                             <xs:annotation>
-                                <xs:documentation>Initial size of a memory region reserved for system needs.</xs:documentation>
+                                <xs:documentation>Initial size of a memory region reserved for system needs. Obsolete, use systemDataRegionConfiguration</xs:documentation>
                             </xs:annotation>
                         </xs:attribute>
                         <xs:attribute name="systemRegionMaxSize" type="xs:int">
                             <xs:annotation>
-                                <xs:documentation>Maximum size of a memory region reserved for system needs.</xs:documentation>
+                                <xs:documentation>Maximum size of a memory region reserved for system needs. Obsolete, use systemDataRegionConfiguration</xs:documentation>
                             </xs:annotation>
                         </xs:attribute>
                         <xs:attribute name="concurrencyLevel" type="xs:int">
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs
index cb96d2e..b62faab 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs
@@ -74,6 +74,9 @@
         /** Platform cache. */
         private readonly IPlatformCache _platformCache;
 
+        /** Whether persistence is enabled for this cache. */
+        private readonly bool _persistenceEnabled;
+
         /// <summary>
         /// Constructor.
         /// </summary>
@@ -99,6 +102,8 @@
 
             _readException = stream => ReadException(Marshaller.StartUnmarshal(stream));
 
+            _persistenceEnabled = target.InLongOutLong((int)CacheOp.PersistenceEnabled, 0) == True;
+
             if (configuration.PlatformCacheConfiguration != null)
             {
                 _platformCache = _ignite.PlatformCacheManager.GetOrCreatePlatformCache(configuration);
@@ -1627,7 +1632,8 @@
                 var scan = qry as ScanQuery<TK, TV>;
 
                 // Local scan with Partition can be satisfied directly from platform cache on server nodes.
-                if (scan != null && scan.Local && scan.Partition != null)
+                // Does not work with persistence - some entries can be present only on disk.
+                if (scan != null && scan.Local && scan.Partition != null && !_persistenceEnabled)
                 {
                     return ScanPlatformCache(scan);
                 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheOp.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheOp.cs
index d0d01eb..e17b1c0 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheOp.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheOp.cs
@@ -117,6 +117,7 @@
         ClearStatistics = 94,
         PutWithPlatformCache = 95,
         ReservePartition = 96,
-        ReleasePartition = 97
+        ReleasePartition = 97,
+        PersistenceEnabled = 99
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/Platform/PlatformCache.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/Platform/PlatformCache.cs
index c7bb09f..42393c9 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/Platform/PlatformCache.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/Platform/PlatformCache.cs
@@ -238,7 +238,10 @@
             var currentVerBoxed = _affinityTopologyVersionFunc();
             var entryVerBoxed = entry.Version;
 
-            Debug.Assert(currentVerBoxed != null);
+            if (entryVerBoxed == null || currentVerBoxed == null)
+            {
+                return false;
+            }
 
             if (ReferenceEquals(currentVerBoxed, entryVerBoxed))
             {
@@ -246,11 +249,6 @@
                 return true;
             }
 
-            if (entryVerBoxed == null)
-            {
-                return false;
-            }
-
             var entryVer = (AffinityTopologyVersion) entryVerBoxed;
             var currentVer = (AffinityTopologyVersion) currentVerBoxed;
 
@@ -274,7 +272,10 @@
         private object GetBoxedAffinityTopologyVersion(AffinityTopologyVersion ver)
         {
             var currentVerBoxed = _affinityTopologyVersionFunc();
-            return (AffinityTopologyVersion) currentVerBoxed == ver ? currentVerBoxed : ver;
+
+            return currentVerBoxed != null && (AffinityTopologyVersion) currentVerBoxed == ver
+                ? currentVerBoxed
+                : ver;
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
index d8a2196..9feffe2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
@@ -31,6 +31,7 @@
         ServiceInvoke = 5, // The flag is not necessary and exists for legacy reasons
         // DefaultQueryTimeout = 6, // IGNITE-13692
         QueryPartitionsBatchSize = 7,
-        BinaryConfiguration = 8
+        BinaryConfiguration = 8,
+        ServiceInvokeCtx = 10
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
index d23bd72..263f4f6 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
@@ -142,7 +142,23 @@
             Func<ClientResponseContext, T> readFunc,
             Func<ClientStatusCode, string, T> errorFunc = null)
         {
-            return GetSocket().DoOutInOp(opId, writeAction, readFunc, errorFunc);
+            var attempt = 0;
+            List<Exception> errors = null;
+
+            while (true)
+            {
+                try
+                {
+                    return GetSocket().DoOutInOp(opId, writeAction, readFunc, errorFunc);
+                }
+                catch (Exception e)
+                {
+                    if (!HandleOpError(e, opId, ref attempt, ref errors))
+                    {
+                        throw;
+                    }
+                }
+            }
         }
 
         /// <summary>
@@ -156,15 +172,31 @@
             TKey key,
             Func<ClientStatusCode, string, T> errorFunc = null)
         {
-            var socket = GetAffinitySocket(cacheId, key) ?? GetSocket();
+            var attempt = 0;
+            List<Exception> errors = null;
 
-            return socket.DoOutInOp(opId, writeAction, readFunc, errorFunc);
+            while (true)
+            {
+                try
+                {
+                    var socket = GetAffinitySocket(cacheId, key) ?? GetSocket();
+
+                    return socket.DoOutInOp(opId, writeAction, readFunc, errorFunc);
+                }
+                catch (Exception e)
+                {
+                    if (!HandleOpError(e, opId, ref attempt, ref errors))
+                    {
+                        throw;
+                    }
+                }
+            }
         }
 
         /// <summary>
         /// Performs an async send-receive operation with partition awareness.
         /// </summary>
-        public Task<T> DoOutInOpAffinityAsync<T, TKey>(
+        public async Task<T> DoOutInOpAffinityAsync<T, TKey>(
             ClientOp opId,
             Action<ClientRequestContext> writeAction,
             Func<ClientResponseContext, T> readFunc,
@@ -172,18 +204,51 @@
             TKey key,
             Func<ClientStatusCode, string, T> errorFunc = null)
         {
-            var socket = GetAffinitySocket(cacheId, key) ?? GetSocket();
+            var attempt = 0;
+            List<Exception> errors = null;
 
-            return socket.DoOutInOpAsync(opId, writeAction, readFunc, errorFunc);
+            while (true)
+            {
+                try
+                {
+                    var socket = GetAffinitySocket(cacheId, key) ?? GetSocket();
+
+                    return await socket.DoOutInOpAsync(opId, writeAction, readFunc, errorFunc).ConfigureAwait(false);
+                }
+                catch (Exception e)
+                {
+                    if (!HandleOpError(e, opId, ref attempt, ref errors))
+                    {
+                        throw;
+                    }
+                }
+            }
         }
 
         /// <summary>
         /// Performs an async send-receive operation.
         /// </summary>
-        public Task<T> DoOutInOpAsync<T>(ClientOp opId, Action<ClientRequestContext> writeAction,
+        public async Task<T> DoOutInOpAsync<T>(ClientOp opId, Action<ClientRequestContext> writeAction,
             Func<ClientResponseContext, T> readFunc, Func<ClientStatusCode, string, T> errorFunc = null)
         {
-            return GetSocket().DoOutInOpAsync(opId, writeAction, readFunc, errorFunc);
+            var attempt = 0;
+            List<Exception> errors = null;
+
+            while (true)
+            {
+                try
+                {
+                    return await GetSocket().DoOutInOpAsync(opId, writeAction, readFunc, errorFunc)
+                        .ConfigureAwait(false);
+                }
+                catch (Exception e)
+                {
+                    if (!HandleOpError(e, opId, ref attempt, ref errors))
+                    {
+                        throw;
+                    }
+                }
+            }
         }
 
         /// <summary>
@@ -338,7 +403,7 @@
             Justification = "There is no finalizer.")]
         public void Dispose()
         {
-            // Lock order: same as in OnAffinityTopologyVersionChange. 
+            // Lock order: same as in OnAffinityTopologyVersionChange.
             lock (_topologyUpdateLock)
             lock (_socketLock)
             {
@@ -924,5 +989,94 @@
                     ? BinaryNameMapperMode.BasicSimple
                     : BinaryNameMapperMode.BasicFull;
         }
+
+        /// <summary>
+        /// Gets a value indicating whether a failed operation should be retried.
+        /// </summary>
+        /// <param name="exception">Exception that caused the operation to fail.</param>
+        /// <param name="op">Operation code.</param>
+        /// <param name="attempt">Current attempt.</param>
+        /// <returns>
+        /// <c>true</c> if the operation should be retried on another connection, <c>false</c> otherwise.
+        /// </returns>
+        private bool ShouldRetry(Exception exception, ClientOp op, int attempt)
+        {
+            var e = exception;
+
+            while (e != null && !(e is SocketException))
+            {
+                e = e.InnerException;
+            }
+
+            if (e == null)
+            {
+                // Only retry socket exceptions.
+                return false;
+            }
+
+            if (_config.RetryPolicy == null)
+            {
+                return false;
+            }
+
+            if (_config.RetryLimit > 0 && attempt >= _config.RetryLimit)
+            {
+                return false;
+            }
+
+            var publicOpType = op.ToPublicOperationsType();
+
+            if (publicOpType == null)
+            {
+                // System operation.
+                return true;
+            }
+
+            var ctx = new ClientRetryPolicyContext(_config, publicOpType.Value, attempt, exception);
+
+            return _config.RetryPolicy.ShouldRetry(ctx);
+        }
+
+        /// <summary>
+        /// Handles operation error.
+        /// </summary>
+        /// <param name="exception">Error.</param>
+        /// <param name="op">Operation code.</param>
+        /// <param name="attempt">Current attempt.</param>
+        /// <param name="errors">Previous errors.</param>
+        /// <returns>True if the error was handled, false otherwise.</returns>
+        private bool HandleOpError(
+            Exception exception,
+            ClientOp op,
+            ref int attempt,
+            ref List<Exception> errors)
+        {
+            if (!ShouldRetry(exception, op, attempt))
+            {
+                if (errors == null)
+                {
+                    return false;
+                }
+
+                var inner = new AggregateException(errors);
+
+                throw new IgniteClientException(
+                    $"Operation failed after {attempt} retries, examine InnerException for details.",
+                    inner);
+            }
+
+            if (errors == null)
+            {
+                errors = new List<Exception> { exception };
+            }
+            else
+            {
+                errors.Add(exception);
+            }
+
+            attempt++;
+
+            return true;
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
index 87b4718..0745d7c 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
@@ -92,6 +92,8 @@
 
         // Services.
         ServiceInvoke = 7000,
+        ServiceGetDescriptors = 7001,
+        ServiceGetDescriptor = 7002,
 
         // Data Streamer.
         DataStreamerStart = 8000,
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs
new file mode 100644
index 0000000..91357cc
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Impl.Client
+{
+    using Apache.Ignite.Core.Client;
+
+    /// <summary>
+    /// Extensions for <see cref="ClientOp"/>.
+    /// </summary>
+    internal static class ClientOpExtensions
+    {
+        /// <summary>
+        /// Converts the internal op code to a public operation type.
+        /// </summary>
+        /// <param name="op">Operation code.</param>
+        /// <returns>Operation type.</returns>
+        public static ClientOperationType? ToPublicOperationsType(this ClientOp op)
+        {
+            switch (op)
+            {
+                case ClientOp.CacheGetOrCreateWithName:
+                case ClientOp.CacheGetOrCreateWithConfiguration:
+                    return ClientOperationType.CacheGetOrCreate;
+
+                case ClientOp.CacheCreateWithConfiguration:
+                case ClientOp.CacheCreateWithName:
+                    return ClientOperationType.CacheCreate;
+
+                case ClientOp.CachePut:
+                    return ClientOperationType.CachePut;
+
+                case ClientOp.CacheGet:
+                    return ClientOperationType.CacheGet;
+
+                case ClientOp.CacheGetNames:
+                    return ClientOperationType.CacheGetNames;
+
+                case ClientOp.CacheDestroy:
+                    return ClientOperationType.CacheDestroy;
+
+                case ClientOp.CacheContainsKey:
+                    return ClientOperationType.CacheContainsKey;
+
+                case ClientOp.CacheContainsKeys:
+                    return ClientOperationType.CacheContainsKeys;
+
+                case ClientOp.CacheGetConfiguration:
+                    return ClientOperationType.CacheGetConfiguration;
+
+                case ClientOp.CacheGetSize:
+                    return ClientOperationType.CacheGetSize;
+
+                case ClientOp.CachePutAll:
+                    return ClientOperationType.CachePutAll;
+
+                case ClientOp.CacheGetAll:
+                    return ClientOperationType.CacheGetAll;
+
+                case ClientOp.CacheReplaceIfEquals:
+                case ClientOp.CacheReplace:
+                    return ClientOperationType.CacheReplace;
+
+                case ClientOp.CacheRemoveKey:
+                case ClientOp.CacheRemoveIfEquals:
+                    return ClientOperationType.CacheRemoveOne;
+
+                case ClientOp.CacheRemoveKeys:
+                    return ClientOperationType.CacheRemoveMultiple;
+
+                case ClientOp.CacheRemoveAll:
+                    return ClientOperationType.CacheRemoveEverything;
+
+                case ClientOp.CacheGetAndPut:
+                    return ClientOperationType.CacheGetAndPut;
+
+                case ClientOp.CacheGetAndRemove:
+                    return ClientOperationType.CacheGetAndRemove;
+
+                case ClientOp.CacheGetAndReplace:
+                    return ClientOperationType.CacheGetAndReplace;
+
+                case ClientOp.CachePutIfAbsent:
+                    return ClientOperationType.CachePutIfAbsent;
+
+                case ClientOp.CacheGetAndPutIfAbsent:
+                    return ClientOperationType.CacheGetAndPutIfAbsent;
+
+                case ClientOp.CacheClear:
+                    return ClientOperationType.CacheClearEverything;
+
+                case ClientOp.CacheClearKey:
+                    return ClientOperationType.CacheClearOne;
+
+                case ClientOp.CacheClearKeys:
+                    return ClientOperationType.CacheClearMultiple;
+
+                case ClientOp.QueryScan:
+                    return ClientOperationType.QueryScan;
+
+                case ClientOp.QuerySql:
+                case ClientOp.QuerySqlFields:
+                    return ClientOperationType.QuerySql;
+
+                case ClientOp.QueryContinuous:
+                    return ClientOperationType.QueryContinuous;
+
+                case ClientOp.TxStart:
+                    return ClientOperationType.TransactionStart;
+
+                case ClientOp.ClusterIsActive:
+                    return ClientOperationType.ClusterGetState;
+
+                case ClientOp.ClusterChangeState:
+                    return ClientOperationType.ClusterChangeState;
+
+                case ClientOp.ClusterGetWalState:
+                    return ClientOperationType.ClusterGetWalState;
+
+                case ClientOp.ClusterChangeWalState:
+                    return ClientOperationType.ClusterChangeWalState;
+
+                case ClientOp.ClusterGroupGetNodeIds:
+                case ClientOp.ClusterGroupGetNodesInfo:
+                    return ClientOperationType.ClusterGroupGetNodes;
+
+                case ClientOp.ComputeTaskExecute:
+                    return ClientOperationType.ComputeTaskExecute;
+
+                case ClientOp.ServiceInvoke:
+                    return ClientOperationType.ServiceInvoke;
+
+                case ClientOp.ServiceGetDescriptors:
+                    return ClientOperationType.ServiceGetDescriptors;
+
+                case ClientOp.ServiceGetDescriptor:
+                    return ClientOperationType.ServiceGetDescriptor;
+
+                default:
+                    return null;
+            }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRetryPolicyContext.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRetryPolicyContext.cs
new file mode 100644
index 0000000..a1463df
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRetryPolicyContext.cs
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Impl.Client
+{
+    using System;
+    using Apache.Ignite.Core.Client;
+
+    /// <summary>
+    /// Retry policy context.
+    /// </summary>
+    internal sealed class ClientRetryPolicyContext : IClientRetryPolicyContext
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientRetryPolicyContext"/> class.
+        /// </summary>
+        /// <param name="configuration">Configuration.</param>
+        /// <param name="operation">Operation.</param>
+        /// <param name="iteration">Iteration.</param>
+        /// <param name="exception">Exception.</param>
+        public ClientRetryPolicyContext(
+            IgniteClientConfiguration configuration,
+            ClientOperationType operation,
+            int iteration,
+            Exception exception)
+        {
+            Configuration = configuration;
+            Operation = operation;
+            Iteration = iteration;
+            Exception = exception;
+        }
+
+        /** <inheritDoc /> */
+        public IgniteClientConfiguration Configuration { get; }
+
+        /** <inheritDoc /> */
+        public ClientOperationType Operation { get; }
+
+        /** <inheritDoc /> */
+        public int Iteration { get; }
+
+        /** <inheritDoc /> */
+        public Exception Exception { get; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ClientServiceDescriptor.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ClientServiceDescriptor.cs
new file mode 100644
index 0000000..fe09f3c
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ClientServiceDescriptor.cs
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Core.Impl.Client.Services
+{
+    using System;
+    using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Core.Client.Services;
+
+    /// <summary>
+    /// Implementation of client service descriptor.
+    /// </summary>
+    internal class ClientServiceDescriptor : IClientServiceDescriptor
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ClientServiceDescriptor" /> class.
+        /// </summary>
+        /// <param name="reader">Reader.</param>
+        public ClientServiceDescriptor(IBinaryRawReader reader)
+        {
+            if (reader == null)
+                throw new ArgumentNullException(nameof(reader));
+
+            Name = reader.ReadString();
+            ServiceClass = reader.ReadString();
+            TotalCount = reader.ReadInt();
+            MaxPerNodeCount = reader.ReadInt();
+            CacheName = reader.ReadString();
+            OriginNodeId = reader.ReadGuid();
+            PlatformType = (Platform.PlatformType) reader.ReadByte();
+        }
+
+        /** <inheritdoc /> */
+        public string Name { get; private set; }
+
+        /** <inheritdoc /> */
+        public string ServiceClass { get; private set; }
+
+        /** <inheritdoc /> */
+        public int TotalCount { get; private set; }
+
+        /** <inheritdoc /> */
+        public int MaxPerNodeCount { get; private set; }
+
+        /** <inheritdoc /> */
+        public string CacheName { get; private set; }
+
+        /** <inheritdoc /> */
+        public Guid? OriginNodeId { get; private set; }
+
+        /** <inheritdoc /> */
+        public Platform.PlatformType PlatformType { get; private set; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ServicesClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ServicesClient.cs
index a650b21..272fce3 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ServicesClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Services/ServicesClient.cs
@@ -18,6 +18,8 @@
 namespace Apache.Ignite.Core.Impl.Client.Services
 {
     using System;
+    using System.Collections.Generic;
+    using System.Collections;
     using System.Diagnostics;
     using System.Reflection;
     using Apache.Ignite.Core.Client;
@@ -25,6 +27,8 @@
     using Apache.Ignite.Core.Impl.Binary;
     using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.Impl.Services;
+    using Apache.Ignite.Core.Platform;
+    using Apache.Ignite.Core.Services;
 
     /// <summary>
     /// Services client.
@@ -84,9 +88,50 @@
         /** <inheritdoc /> */
         public T GetServiceProxy<T>(string serviceName) where T : class
         {
-            IgniteArgumentCheck.NotNullOrEmpty(serviceName, "name");
+            return GetServiceProxy<T>(serviceName, null);
+        }
 
-            return ServiceProxyFactory<T>.CreateProxy((method, args) => InvokeProxyMethod(serviceName, method, args));
+        /** <inheritdoc /> */
+        public T GetServiceProxy<T>(string serviceName, IServiceCallContext callCtx) where T : class
+        {
+            IgniteArgumentCheck.NotNullOrEmpty(serviceName, "name");
+            IgniteArgumentCheck.Ensure(callCtx == null || callCtx is ServiceCallContext, "callCtx",
+                "custom implementation of " + typeof(ServiceCallContext).Name + " is not supported." +
+                " Please use " + typeof(ServiceCallContextBuilder).Name + " to create it.");
+
+            var platformType = GetServiceDescriptor(serviceName).PlatformType;
+            IDictionary callAttrs = callCtx == null ? null : ((ServiceCallContext) callCtx).Values();
+
+            return ServiceProxyFactory<T>.CreateProxy(
+                (method, args) => InvokeProxyMethod(serviceName, method, args, platformType, callAttrs)
+            );
+        }
+
+        /** <inheritdoc /> */
+        public ICollection<IClientServiceDescriptor> GetServiceDescriptors()
+        {
+            return _ignite.Socket.DoOutInOp(
+                ClientOp.ServiceGetDescriptors,
+                ctx => { },
+                ctx =>
+                {
+                    var cnt = ctx.Reader.ReadInt();
+                    var res = new List<IClientServiceDescriptor>(cnt);
+
+                    for (var i = 0; i < cnt; i++)
+                        res.Add(new ClientServiceDescriptor(ctx.Reader));
+
+                    return res;
+                });
+        }
+
+        /** <inheritdoc /> */
+        public IClientServiceDescriptor GetServiceDescriptor(string serviceName)
+        {
+            return _ignite.Socket.DoOutInOp(
+                ClientOp.ServiceGetDescriptor,
+                ctx => ctx.Writer.WriteString(serviceName),
+                ctx => new ClientServiceDescriptor(ctx.Reader));
         }
 
         /** <inheritdoc /> */
@@ -104,10 +149,10 @@
         /// <summary>
         /// Invokes the proxy method.
         /// </summary>
-        private object InvokeProxyMethod(string serviceName, MethodBase method, object[] args)
+        private object InvokeProxyMethod(string serviceName, MethodBase method, object[] args,
+            PlatformType platformType, IDictionary callAttrs)
         {
-            return _ignite.Socket.DoOutInOp(
-                ClientOp.ServiceInvoke,
+            return _ignite.Socket.DoOutInOp(ClientOp.ServiceInvoke,
                 ctx =>
                 {
                     var w = ctx.Writer;
@@ -138,10 +183,16 @@
 
                     w.WriteString(method.Name);
 
-                    w.WriteInt(args.Length);
-                    foreach (var arg in args)
+                    ServiceProxySerializer.WriteMethodArguments(w, null, args, platformType);
+
+                    if (ctx.Features.HasFeature(ClientBitmaskFeature.ServiceInvokeCtx))
                     {
-                        w.WriteObjectDetached(arg);
+                        w.WriteDictionary(callAttrs);
+                    }
+                    else if (callAttrs != null)
+                    {
+                        throw new IgniteClientException(
+                            "Passing caller context to the service is not supported by the server");
                     }
                 },
                 ctx =>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/TypeCaster.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/TypeCaster.cs
index 4f41110..e5d4c34 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/TypeCaster.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/TypeCaster.cs
@@ -22,7 +22,7 @@
     using System.Linq.Expressions;
 
     /// <summary>
-    /// Does type casts without extra boxing. 
+    /// Does type casts without extra boxing.
     /// Should be used when casting compile-time incompatible value types instead of "(T)(object)x".
     /// </summary>
     /// <typeparam name="T">Target type</typeparam>
@@ -62,7 +62,7 @@
             /// <summary>
             /// Compiled caster delegate.
             /// </summary>
-            [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", 
+            [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields",
                 Justification = "Incorrect warning")]
             [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes",
                 Justification = "Intended usage to leverage compiler caching.")]
@@ -87,10 +87,40 @@
                 }
 
                 var paramExpr = Expression.Parameter(typeof(TFrom));
-                var convertExpr = Expression.Convert(paramExpr, typeof(T));
+
+                var convertExpr = typeof(TFrom) == typeof(Enum)
+                    ? TryConvertRawEnum(typeof(T), paramExpr)
+                    : Expression.Convert(paramExpr, typeof(T));
 
                 return Expression.Lambda<Func<TFrom, T>>(convertExpr, paramExpr).Compile();
             }
+
+            private static Expression TryConvertRawEnum(Type toType, Expression fromParamExpr)
+            {
+                var mtdName = "";
+
+                if (toType == typeof(byte))
+                    mtdName = "ToByte";
+                else if (toType == typeof(sbyte))
+                    mtdName = "ToSByte";
+                else if (toType == typeof(short))
+                    mtdName = "ToInt16";
+                else if (toType == typeof(ushort))
+                    mtdName = "ToUInt16";
+                else if (toType == typeof(int))
+                    mtdName = "ToInt32";
+                else if (toType == typeof(uint))
+                    mtdName = "ToUInt32";
+
+                var toIntMtd = typeof(Convert).GetMethod(mtdName, new[] {typeof(object)});
+
+                if (toIntMtd == null)
+                {
+                    throw new InvalidCastException($"Unable to convert 'System.Enum' to '{toType}': no converter found.");
+                }
+
+                return Expression.Call(null, toIntMtd, fromParamExpr);
+            }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceDescriptor.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceDescriptor.cs
index 6bf5c0c..b7b6e69 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceDescriptor.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceDescriptor.cs
@@ -22,7 +22,7 @@
     using System.Diagnostics;
     using Apache.Ignite.Core.Impl.Binary;
     using Apache.Ignite.Core.Impl.Collections;
-    using Apache.Ignite.Core.Impl.Common;
+    using Apache.Ignite.Core.Platform;
     using Apache.Ignite.Core.Services;
 
     /// <summary>
@@ -110,4 +110,4 @@
         /** <inheritdoc /> */
         public IDictionary<Guid, int> TopologySnapshot { get; private set; }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxyInvoker.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxyInvoker.cs
index 006aa00..9624e49 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxyInvoker.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxyInvoker.cs
@@ -107,6 +107,13 @@
             if (methods.Length == 1)
                 return (obj, args) => methods[0].Invoke(obj, args);
 
+            if (methods.Length > 1)
+                // Try to search applicable without equality of all arguments.
+                methods = methods.Where(m => AreMethodArgsCompatible(arguments, m.GetParameters(), true)).ToArray();
+
+            if (methods.Length == 1)
+                return (obj, args) => methods[0].Invoke(obj, args);
+
             // 3) 0 or more than 1 matching method - throw.
             var argsString = argsLength == 0
                 ? "0"
@@ -146,8 +153,10 @@
         /// </summary>
         /// <param name="methodArgs">Method argument types.</param>
         /// <param name="targetParameters">Target method parameter definitions.</param>
+        /// <param name="strictTypeCheck">If true check argument type for equality.</param>
         /// <returns>True if a target method can be called with specified set of arguments; otherwise, false.</returns>
-        private static bool AreMethodArgsCompatible(object[] methodArgs, ParameterInfo[] targetParameters)
+        private static bool AreMethodArgsCompatible(object[] methodArgs, ParameterInfo[] targetParameters,
+            bool strictTypeCheck = false)
         {
             if (methodArgs == null || methodArgs.Length == 0)
                 return targetParameters.Length == 0;
@@ -155,9 +164,19 @@
             if (methodArgs.Length != targetParameters.Length)
                 return false;
 
+            Func<object, ParameterInfo, bool> checker;
+            if (strictTypeCheck)
+            {
+                checker = (arg, param) => arg == null || param.ParameterType == arg.GetType();
+            }
+            else
+            {
+                checker = (arg, param) => arg == null || param.ParameterType.IsInstanceOfType(arg);
+            }
+
             return methodArgs
-                .Zip(targetParameters, (arg, param) => arg == null || param.ParameterType.IsInstanceOfType(arg))
+                .Zip(targetParameters, checker)
                 .All(x => x);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxySerializer.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxySerializer.cs
index 5614fd5..d987d74 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxySerializer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/ServiceProxySerializer.cs
@@ -25,7 +25,7 @@
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Impl.Binary;
     using Apache.Ignite.Core.Impl.Binary.IO;
-    using Apache.Ignite.Core.Impl.Common;
+    using Apache.Ignite.Core.Platform;
     using Apache.Ignite.Core.Services;
 
     /// <summary>
@@ -41,9 +41,9 @@
         /// <param name="method">Method (optional, can be null).</param>
         /// <param name="arguments">Arguments.</param>
         /// <param name="platformType">The platform.</param>
-        /// <param name="callCtx">Service call context.</param>
+        /// <param name="callAttrs">Service call context attributes.</param>
         public static void WriteProxyMethod(BinaryWriter writer, string methodName, MethodBase method,
-            object[] arguments, PlatformType platformType, IServiceCallContext callCtx)
+            object[] arguments, PlatformType platformType, IDictionary callAttrs)
         {
             Debug.Assert(writer != null);
 
@@ -52,6 +52,24 @@
             if (arguments != null)
             {
                 writer.WriteBoolean(true);
+
+                WriteMethodArguments(writer, method, arguments, platformType);
+            }
+            else
+                writer.WriteBoolean(false);
+
+            writer.WriteDictionary(callAttrs);
+        }
+
+        /// <summary>
+        /// Writes method arguments like required for specific platform.
+        /// </summary>
+        /// <param name="writer">Writer.</param>
+        /// <param name="method">Method (optional, can be null).</param>
+        /// <param name="arguments">Arguments.</param>
+        /// <param name="platformType">The platform.</param>
+        public static void WriteMethodArguments(BinaryWriter writer, MethodBase method, object[] arguments, PlatformType platformType)
+        {
                 writer.WriteInt(arguments.Length);
 
                 if (platformType == PlatformType.DotNet)
@@ -74,11 +92,6 @@
                             arguments[i]);
                     }
                 }
-            }
-            else
-                writer.WriteBoolean(false);
-
-            writer.WriteDictionary(callCtx == null ? null : ((ServiceCallContext) callCtx).Values());
         }
 
         /// <summary>
@@ -271,7 +284,7 @@
             if (handler != null)
                 return null;
 
-            if (type.IsArray)
+            if (type.IsArray || arg.GetType().IsArray)
                 return (writer, o) => writer.WriteArrayInternal((Array) o);
 
             if (arg is IDictionary)
@@ -283,4 +296,4 @@
             return null;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
index 7ec4b3a..eeb4b96 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Core.Impl.Services
 {
+    using System.Collections;
     using System.Collections.Generic;
     using System.Diagnostics;
     using System.Linq;
@@ -26,6 +27,7 @@
     using Apache.Ignite.Core.Impl.Binary;
     using Apache.Ignite.Core.Impl.Binary.IO;
     using Apache.Ignite.Core.Impl.Common;
+    using Apache.Ignite.Core.Platform;
     using Apache.Ignite.Core.Services;
 
     /// <summary>
@@ -390,9 +392,10 @@
             });
 
             var platform = GetServiceDescriptors().Cast<ServiceDescriptor>().Single(x => x.Name == name).PlatformType;
+            var callAttrs = GetCallerContextAttributes(callCtx);
 
             return ServiceProxyFactory<T>.CreateProxy((method, args) =>
-                InvokeProxyMethod(javaProxy, method.Name, method, args, platform, callCtx));
+                InvokeProxyMethod(javaProxy, method.Name, method, args, platform, callAttrs));
         }
 
         /** <inheritDoc /> */
@@ -428,9 +431,10 @@
             });
 
             var platform = GetServiceDescriptors().Cast<ServiceDescriptor>().Single(x => x.Name == name).PlatformType;
+            var callAttrs = GetCallerContextAttributes(callCtx);
 
             return new DynamicServiceProxy((methodName, args) =>
-                InvokeProxyMethod(javaProxy, methodName, null, args, platform, callCtx));
+                InvokeProxyMethod(javaProxy, methodName, null, args, platform, callAttrs));
         }
 
         /// <summary>
@@ -441,12 +445,12 @@
         /// <param name="method">Method to invoke.</param>
         /// <param name="args">Arguments.</param>
         /// <param name="platformType">The platform.</param>
-        /// <param name="callCtx">Service call context.</param>
+        /// <param name="callAttrs">Service call context attributes.</param>
         /// <returns>
         /// Invocation result.
         /// </returns>
         private object InvokeProxyMethod(IPlatformTargetInternal proxy, string methodName,
-            MethodBase method, object[] args, PlatformType platformType, IServiceCallContext callCtx)
+            MethodBase method, object[] args, PlatformType platformType, IDictionary callAttrs)
         {
             bool locRegisterSameJavaType = Marshaller.RegisterSameJavaTypeTl.Value;
 
@@ -458,7 +462,7 @@
             try
             {
                 return DoOutInOp(OpInvokeMethod,
-                    writer => ServiceProxySerializer.WriteProxyMethod(writer, methodName, method, args, platformType, callCtx),
+                    writer => ServiceProxySerializer.WriteProxyMethod(writer, methodName, method, args, platformType, callAttrs),
                     (stream, res) => ServiceProxySerializer.ReadInvocationResult(stream, Marshaller, _keepBinary),
                     proxy);
             }
@@ -490,6 +494,20 @@
         }
 
         /// <summary>
+        /// Gets the attributes of the service call context.
+        /// </summary>
+        /// <param name="callCtx">Service call context.</param>
+        /// <returns>Service call context attributes.</returns>
+        private IDictionary GetCallerContextAttributes(IServiceCallContext callCtx)
+        {
+            IgniteArgumentCheck.Ensure(callCtx == null || callCtx is ServiceCallContext, "callCtx", 
+                "custom implementation of " + typeof(ServiceCallContext).Name + " is not supported." +
+                " Please use " + typeof(ServiceCallContextBuilder).Name + " to create it.");
+
+            return callCtx == null ? null : ((ServiceCallContext) callCtx).Values();
+        }
+
+        /// <summary>
         /// Performs ServiceConfiguration validation.
         /// </summary>
         /// <param name="configuration">Service configuration</param>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Platform/PlatformType.cs
similarity index 89%
rename from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
rename to modules/platforms/dotnet/Apache.Ignite.Core/Platform/PlatformType.cs
index e09e31b..cb3adae 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/PlatformType.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Platform/PlatformType.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,12 +15,12 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Common
+namespace Apache.Ignite.Core.Platform
 {
     /// <summary>
-    /// Represents an Ignite platform.
+    /// Interop platform type.
     /// </summary>
-    internal enum PlatformType
+    public enum PlatformType
     {
         /// <summary>
         /// Java platform.
diff --git a/modules/rest-http/README.txt b/modules/rest-http/README.txt
index 0df2cf5..e7a855e 100644
--- a/modules/rest-http/README.txt
+++ b/modules/rest-http/README.txt
@@ -8,6 +8,9 @@
 'libs' folder before running 'ignite.{sh|bat}' script. The content of the module folder will
 be added to classpath in this case.
 
+The module depends on third-party libraries that use the slf4j facade for logging.
+You can set up an underlying logging framework yourself.
+
 Importing REST-HTTP Module In Maven Project
 -------------------------------------------
 
diff --git a/modules/rest-http/pom.xml b/modules/rest-http/pom.xml
index 49a0396..8ba1c97 100644
--- a/modules/rest-http/pom.xml
+++ b/modules/rest-http/pom.xml
@@ -121,14 +121,9 @@
         </dependency>
 
         <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-log4j12</artifactId>
-            <version>${slf4j.version}</version>
-        </dependency>
-
-        <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
+            <scope>test</scope>
         </dependency>
 
         <dependency>
diff --git a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyObjectMapper.java b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyObjectMapper.java
index d392bcb..fc3966a 100644
--- a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyObjectMapper.java
+++ b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyObjectMapper.java
@@ -45,6 +45,7 @@
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.binary.BinaryType;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryEnumObjectImpl;
 import org.apache.ignite.internal.binary.BinaryObjectImpl;
 import org.apache.ignite.internal.processors.cache.query.GridCacheSqlIndexMetadata;
@@ -82,6 +83,7 @@
         module.addSerializer(GridCacheSqlMetadata.class, IGNITE_SQL_METADATA_SERIALIZER);
         module.addSerializer(GridCacheSqlIndexMetadata.class, IGNITE_SQL_INDEX_METADATA_SERIALIZER);
         module.addSerializer(BinaryObjectImpl.class, IGNITE_BINARY_OBJECT_SERIALIZER);
+        module.addSerializer(BinaryArray.class, IGNITE_BINARY_ARRAY_SERIALIZER);
 
         // Standard serializer loses nanoseconds.
         module.addSerializer(Timestamp.class, IGNITE_TIMESTAMP_SERIALIZER);
@@ -304,6 +306,19 @@
         }
     };
 
+    /** Custom serializer for {@link BinaryArray}. */
+    private static final JsonSerializer<BinaryArray> IGNITE_BINARY_ARRAY_SERIALIZER = new JsonSerializer<BinaryArray>() {
+        /** {@inheritDoc} */
+        @Override public void serialize(BinaryArray val, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            gen.writeStartArray();
+
+            for (Object o : val.array())
+                gen.writeObject(o);
+
+            gen.writeEndArray();
+        }
+    };
+
     /** Custom serializer for {@link java.sql.Timestamp}. */
     private static final JsonSerializer<Timestamp> IGNITE_TIMESTAMP_SERIALIZER = new JsonSerializer<Timestamp>() {
         /** {@inheritDoc} */
diff --git a/modules/spring/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelperImpl.java b/modules/spring/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelperImpl.java
index 61a2f0d..3963d1f 100644
--- a/modules/spring/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelperImpl.java
+++ b/modules/spring/src/main/java/org/apache/ignite/internal/util/spring/IgniteSpringHelperImpl.java
@@ -37,10 +37,8 @@
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
-import org.jetbrains.annotations.Nullable;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.PropertyValue;
-import org.springframework.beans.factory.ListableBeanFactory;
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinitionHolder;
@@ -145,17 +143,24 @@
     }
 
     /** {@inheritDoc} */
-    @Override public Map<Class<?>, Object> loadBeans(URL cfgUrl, Class<?>... beanClasses) throws IgniteCheckedException {
+    @Override public IgniteBiTuple<Map<Class<?>, Collection>, ? extends GridSpringResourceContext> loadBeans(
+        URL cfgUrl,
+        Class<?>... beanClasses
+    ) throws IgniteCheckedException {
         assert beanClasses.length > 0;
 
         ApplicationContext springCtx = initContext(cfgUrl);
 
-        Map<Class<?>, Object> beans = new HashMap<>();
+        Map<Class<?>, Collection> beans = new HashMap<>();
 
-        for (Class<?> cls : beanClasses)
-            beans.put(cls, bean(springCtx, cls));
+        for (Class<?> cls : beanClasses) {
+            Map<String, ?> clsBeans = springCtx.getBeansOfType(cls);
 
-        return beans;
+            if (clsBeans != null)
+                beans.put(cls, clsBeans.values());
+        }
+
+        return F.t(beans, new GridSpringResourceContextImpl(springCtx));
     }
 
     /** {@inheritDoc} */
@@ -177,21 +182,6 @@
     }
 
     /** {@inheritDoc} */
-    @Override public Map<Class<?>, Object> loadBeans(InputStream cfgStream, Class<?>... beanClasses)
-        throws IgniteCheckedException {
-        assert beanClasses.length > 0;
-
-        ApplicationContext springCtx = initContext(cfgStream);
-
-        Map<Class<?>, Object> beans = new HashMap<>();
-
-        for (Class<?> cls : beanClasses)
-            beans.put(cls, bean(springCtx, cls));
-
-        return beans;
-    }
-
-    /** {@inheritDoc} */
     @SuppressWarnings("unchecked")
     @Override public <T> T loadBean(InputStream cfgStream, String beanName) throws IgniteCheckedException {
         ApplicationContext springCtx = initContext(cfgStream);
@@ -348,19 +338,6 @@
     }
 
     /**
-     * Gets bean configuration.
-     *
-     * @param ctx Spring context.
-     * @param beanCls Bean class.
-     * @return Spring bean.
-     */
-    @Nullable private static <T> T bean(ListableBeanFactory ctx, Class<T> beanCls) {
-        Map.Entry<String, T> entry = F.firstEntry(ctx.getBeansOfType(beanCls));
-
-        return entry == null ? null : entry.getValue();
-    }
-
-    /**
      * Creates Spring application context. Optionally excluded properties can be specified,
      * it means that if such a property is found in {@link org.apache.ignite.configuration.IgniteConfiguration}
      * then it is removed before the bean is instantiated.
diff --git a/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceInjectionSelfTest.java b/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceInjectionSelfTest.java
index 9d70944..160b313 100644
--- a/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceInjectionSelfTest.java
+++ b/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceInjectionSelfTest.java
@@ -109,9 +109,6 @@
             @Override public Object call() throws Exception {
                 assertNotNull(svc);
 
-                // Ensure proxy instance.
-                assertTrue(svc instanceof Service);
-
                 svc.noop();
 
                 return null;
@@ -205,9 +202,6 @@
             private void service(DummyService svc) {
                 assertNotNull(svc);
 
-                // Ensure proxy instance.
-                assertTrue(svc instanceof Service);
-
                 this.svc = svc;
             }
 
diff --git a/modules/zookeeper/pom.xml b/modules/zookeeper/pom.xml
index 771fb6e..f993082 100644
--- a/modules/zookeeper/pom.xml
+++ b/modules/zookeeper/pom.xml
@@ -42,53 +42,15 @@
         </dependency>
 
         <dependency>
-            <groupId>org.apache.curator</groupId>
-            <artifactId>curator-framework</artifactId>
-            <version>${curator.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.curator</groupId>
-            <artifactId>curator-recipes</artifactId>
-            <version>${curator.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.curator</groupId>
-            <artifactId>curator-x-discovery</artifactId>
-            <version>${curator.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.curator</groupId>
-            <artifactId>curator-client</artifactId>
-            <version>${curator.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <version>${guava16.version}</version>
-        </dependency>
-
-        <!-- Do not remove org.codehaus.jackson:jackson-core-asl it is required by Apache Curator at runtime -->
-        <dependency>
-            <groupId>org.codehaus.jackson</groupId>
-            <artifactId>jackson-core-asl</artifactId>
-            <version>${jackson1.version}</version>
-        </dependency>
-
-        <!-- Do not remove org.codehaus.jackson:jackson-mapper-asl it is required by Apache Curator at runtime -->
-        <dependency>
-            <groupId>org.codehaus.jackson</groupId>
-            <artifactId>jackson-mapper-asl</artifactId>
-            <version>${jackson1.version}</version>
-        </dependency>
-
-        <dependency>
             <groupId>org.apache.zookeeper</groupId>
             <artifactId>zookeeper</artifactId>
             <version>${zookeeper.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
@@ -107,11 +69,20 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-log4j12</artifactId>
             <version>${slf4j.version}</version>
+            <scope>test</scope>
         </dependency>
 
         <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-framework</artifactId>
+            <version>${curator.version}</version>
+            <scope>test</scope>
         </dependency>
 
         <dependency>
diff --git a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/TcpDiscoveryZookeeperIpFinder.java b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/TcpDiscoveryZookeeperIpFinder.java
deleted file mode 100644
index ad0821b..0000000
--- a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/TcpDiscoveryZookeeperIpFinder.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/*
- * 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.spi.discovery.tcp.ipfinder.zk;
-
-import java.net.InetSocketAddress;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import com.google.common.collect.Sets;
-import org.apache.curator.RetryPolicy;
-import org.apache.curator.framework.CuratorFramework;
-import org.apache.curator.framework.CuratorFrameworkFactory;
-import org.apache.curator.framework.imps.CuratorFrameworkState;
-import org.apache.curator.retry.ExponentialBackoffRetry;
-import org.apache.curator.x.discovery.ServiceDiscovery;
-import org.apache.curator.x.discovery.ServiceDiscoveryBuilder;
-import org.apache.curator.x.discovery.ServiceInstance;
-import org.apache.curator.x.discovery.UriSpec;
-import org.apache.curator.x.discovery.details.JsonInstanceSerializer;
-import org.apache.ignite.IgniteLogger;
-import org.apache.ignite.IgniteSystemProperties;
-import org.apache.ignite.SystemProperty;
-import org.apache.ignite.internal.util.tostring.GridToStringExclude;
-import org.apache.ignite.internal.util.typedef.internal.A;
-import org.apache.ignite.internal.util.typedef.internal.U;
-import org.apache.ignite.resources.LoggerResource;
-import org.apache.ignite.spi.IgniteSpiException;
-import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinderAdapter;
-import org.codehaus.jackson.map.annotate.JsonRootName;
-
-/**
- * This TCP Discovery IP Finder uses Apache ZooKeeper (ZK) to locate peer nodes when bootstrapping in order to join
- * the cluster. It uses the Apache Curator library to interact with ZooKeeper in a simple manner. Specifically,
- * it uses the {@link ServiceDiscovery} recipe, which makes use of ephemeral nodes in ZK to register services.
- *
- * <p>
- * There are several ways to instantiate the TcpDiscoveryZookeeperIpFinder:
- * <li>
- *     <ul>By providing an instance of {@link CuratorFramework} directly, in which case no ZK Connection String
- *     is required.</ul>
- *     <ul>By providing a ZK Connection String through {@link #setZkConnectionString(String)}, and optionally
- *     a {@link RetryPolicy} through the setter. If the latter is not provided, a default
- *     {@link ExponentialBackoffRetry} policy is used, with a base sleep time of 1000ms and 10 retries.</ul>
- *     <ul>By providing a ZK Connection String through system property {@link #PROP_ZK_CONNECTION_STRING}. If this
- *     property is set, it overrides the ZK Connection String passed in as a property, but it does not override
- *     the {@link CuratorFramework} if provided.</ul>
- * </li>
- *
- * You may customise the base path for services, as well as the service name. By default {@link #BASE_PATH} and
- * {@link #SERVICE_NAME} are use respectively. You can also choose to enable or disable duplicate registrations. See
- * {@link #setAllowDuplicateRegistrations(boolean)} for more details.
- *
- * @see <a href="http://zookeeper.apache.org">Apache ZooKeeper</a>
- * @see <a href="http://curator.apache.org">Apache Curator</a>
- */
-public class TcpDiscoveryZookeeperIpFinder extends TcpDiscoveryIpFinderAdapter {
-    /** System property name to provide the ZK Connection String. */
-    @SystemProperty(value = "Zookeeper connection string", type = String.class)
-    public static final String PROP_ZK_CONNECTION_STRING = "IGNITE_ZK_CONNECTION_STRING";
-
-    /** Default base path for service registrations. */
-    private static final String BASE_PATH = "/services";
-
-    /** Default service name for service registrations. */
-    private static final String SERVICE_NAME = "ignite";
-
-    /** Default URI Spec to use with the {@link ServiceDiscoveryBuilder}. */
-    private static final UriSpec URI_SPEC = new UriSpec("{address}:{port}");
-
-    /** Init guard. */
-    @GridToStringExclude
-    private final AtomicBoolean initGuard = new AtomicBoolean();
-
-    /** Init guard. */
-    @GridToStringExclude
-    private final AtomicBoolean closeGuard = new AtomicBoolean();
-
-    /** Logger. */
-    @LoggerResource
-    private IgniteLogger log;
-
-    /** The Curator framework in use, either injected or constructed by this component. */
-    private CuratorFramework curator;
-
-    /** The ZK Connection String if provided by the user. */
-    private String zkConnectionString;
-
-    /** Retry policy to use. */
-    private RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
-
-    /** Base path to use, by default {#link #BASE_PATH}. */
-    private String basePath = BASE_PATH;
-
-    /** Service name to use, by default {#link #SERVICE_NAME}. */
-    private String serviceName = SERVICE_NAME;
-
-    /** Whether to allow or not duplicate registrations. See setter doc. */
-    private boolean allowDuplicateRegistrations;
-
-    /** The Service Discovery recipe. */
-    private ServiceDiscovery<IgniteInstanceDetails> discovery;
-
-    /** Map of the {#link ServiceInstance}s we have registered. */
-    private Map<InetSocketAddress, ServiceInstance<IgniteInstanceDetails>> ourInstances = new ConcurrentHashMap<>();
-
-    /** Constructor. */
-    public TcpDiscoveryZookeeperIpFinder() {
-        setShared(true);
-    }
-
-    /** Initializes this IP Finder by creating the appropriate Curator objects. */
-    private void init() {
-        if (!initGuard.compareAndSet(false, true))
-            return;
-
-        String sysPropZkConnString = IgniteSystemProperties.getString(PROP_ZK_CONNECTION_STRING);
-
-        if (sysPropZkConnString != null && !sysPropZkConnString.trim().isEmpty())
-            zkConnectionString = sysPropZkConnString;
-
-        if (log.isInfoEnabled())
-            log.info("Initializing ZooKeeper IP Finder.");
-
-        if (curator == null) {
-            A.notNullOrEmpty(zkConnectionString, String.format("ZooKeeper URL (or system property %s) cannot be null " +
-                "or empty if a CuratorFramework object is not provided explicitly", PROP_ZK_CONNECTION_STRING));
-            curator = CuratorFrameworkFactory.newClient(zkConnectionString, retryPolicy);
-        }
-
-        if (curator.getState() == CuratorFrameworkState.LATENT)
-            curator.start();
-
-        A.ensure(curator.getState() == CuratorFrameworkState.STARTED, "CuratorFramework can't be started.");
-
-        discovery = ServiceDiscoveryBuilder.builder(IgniteInstanceDetails.class)
-            .client(curator)
-            .basePath(basePath)
-            .serializer(new JsonInstanceSerializer<>(IgniteInstanceDetails.class))
-            .build();
-    }
-
-    /** {@inheritDoc} */
-    @Override public void onSpiContextDestroyed() {
-        if (!closeGuard.compareAndSet(false, true)) {
-            U.warn(log, "ZooKeeper IP Finder can't be closed more than once.");
-
-            return;
-        }
-
-        if (log.isInfoEnabled())
-            log.info("Destroying ZooKeeper IP Finder.");
-
-        super.onSpiContextDestroyed();
-
-        if (curator != null)
-            curator.close();
-
-    }
-
-    /** {@inheritDoc} */
-    @Override public Collection<InetSocketAddress> getRegisteredAddresses() throws IgniteSpiException {
-        init();
-
-        if (log.isDebugEnabled())
-            log.debug("Getting registered addresses from ZooKeeper IP Finder.");
-
-        Collection<ServiceInstance<IgniteInstanceDetails>> serviceInstances;
-
-        try {
-            serviceInstances = discovery.queryForInstances(serviceName);
-        }
-        catch (Exception e) {
-            log.warning("Error while getting registered addresses from ZooKeeper IP Finder.", e);
-            return Collections.emptyList();
-        }
-
-        Set<InetSocketAddress> answer = new HashSet<>();
-
-        for (ServiceInstance<IgniteInstanceDetails> si : serviceInstances)
-            answer.add(new InetSocketAddress(si.getAddress(), si.getPort()));
-
-        if (log.isInfoEnabled())
-            log.info("ZooKeeper IP Finder resolved addresses: " + answer);
-
-        return answer;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void registerAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
-        init();
-
-        if (log.isInfoEnabled())
-            log.info("Registering addresses with ZooKeeper IP Finder: " + addrs);
-
-        Set<InetSocketAddress> registrationsToIgnore = Sets.newHashSet();
-        if (!allowDuplicateRegistrations) {
-            try {
-                for (ServiceInstance<IgniteInstanceDetails> sd : discovery.queryForInstances(serviceName))
-                    registrationsToIgnore.add(new InetSocketAddress(sd.getAddress(), sd.getPort()));
-            }
-            catch (Exception e) {
-                log.warning("Error while finding currently registered services to avoid duplicate registrations", e);
-                throw new IgniteSpiException(e);
-            }
-        }
-
-        for (InetSocketAddress addr : addrs) {
-            if (registrationsToIgnore.contains(addr))
-                continue;
-
-            try {
-                ServiceInstance<IgniteInstanceDetails> si = ServiceInstance.<IgniteInstanceDetails>builder()
-                    .name(serviceName)
-                    .uriSpec(URI_SPEC)
-                    .address(addr.getAddress().getHostAddress())
-                    .port(addr.getPort())
-                    .build();
-
-                ourInstances.put(addr, si);
-
-                discovery.registerService(si);
-
-            }
-            catch (Exception e) {
-                log.warning(String.format("Error while registering an address from ZooKeeper IP Finder " +
-                    "[message=%s,addresses=%s]", e.getMessage(), addr), e);
-            }
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override public void unregisterAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
-
-        // if curator is not STARTED, we have nothing to unregister, because we are using ephemeral nodes,
-        // which means that our addresses will only be registered in ZK as long as our connection is alive
-        if (curator.getState() != CuratorFrameworkState.STARTED)
-            return;
-
-        if (log.isInfoEnabled())
-            log.info("Unregistering addresses with ZooKeeper IP Finder: " + addrs);
-
-        for (InetSocketAddress addr : addrs) {
-            ServiceInstance<IgniteInstanceDetails> si = ourInstances.get(addr);
-            if (si == null) {
-                log.warning("Asked to unregister address from ZooKeeper IP Finder, but no match was found in local " +
-                    "instance map for: " + addrs);
-                continue;
-            }
-
-            try {
-                discovery.unregisterService(si);
-            }
-            catch (Exception e) {
-                log.warning("Error while unregistering an address from ZooKeeper IP Finder: " + addr, e);
-            }
-        }
-    }
-
-    /**
-     * @param curator A {@link CuratorFramework} instance to use. It can already be in <tt>STARTED</tt> state.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setCurator(CuratorFramework curator) {
-        this.curator = curator;
-
-        return this;
-    }
-
-    /**
-     * @return The ZooKeeper connection string, only if set explicitly. Else, it returns null.
-     */
-    public String getZkConnectionString() {
-        return zkConnectionString;
-    }
-
-    /**
-     * @param zkConnectionString ZooKeeper connection string in case a {@link CuratorFramework} is not being set
-     * explicitly.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setZkConnectionString(String zkConnectionString) {
-        this.zkConnectionString = zkConnectionString;
-
-        return this;
-    }
-
-    /**
-     * @return Retry policy in use if, and only if, it was set explicitly. Else, it returns null.
-     */
-    public RetryPolicy getRetryPolicy() {
-        return retryPolicy;
-    }
-
-    /**
-     * @param retryPolicy {@link RetryPolicy} to use in case a ZK Connection String is being injected, or if using a
-     * system property.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setRetryPolicy(RetryPolicy retryPolicy) {
-        this.retryPolicy = retryPolicy;
-
-        return this;
-    }
-
-    /**
-     * @return Base path for service registration in ZK. Default value: {@link #BASE_PATH}.
-     */
-    public String getBasePath() {
-        return basePath;
-    }
-
-    /**
-     * @param basePath Base path for service registration in ZK. If not passed, {@link #BASE_PATH} will be used.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setBasePath(String basePath) {
-        this.basePath = basePath;
-
-        return this;
-    }
-
-    /**
-     * @return Service name being used, in Curator terms. See {@link #setServiceName(String)} for more information.
-     */
-    public String getServiceName() {
-        return serviceName;
-    }
-
-    /**
-     * @param serviceName Service name to use, as defined by Curator's {#link ServiceDiscovery} recipe. In physical ZK
-     * terms, it represents the node under {@link #basePath}, under which services will be registered.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setServiceName(String serviceName) {
-        this.serviceName = serviceName;
-
-        return this;
-    }
-
-    /**
-     * @return The value of this flag. See {@link #setAllowDuplicateRegistrations(boolean)} for more details.
-     */
-    public boolean isAllowDuplicateRegistrations() {
-        return allowDuplicateRegistrations;
-    }
-
-    /**
-     * @param allowDuplicateRegistrations Whether to register each node only once, or if duplicate registrations are
-     * allowed. Nodes will attempt to register themselves, plus those they know about. By default, duplicate
-     * registrations are not allowed, but you might want to set this property to <tt>true</tt> if you have multiple
-     * network interfaces or if you are facing troubles.
-     * @return {@code this} for chaining.
-     */
-    public TcpDiscoveryZookeeperIpFinder setAllowDuplicateRegistrations(boolean allowDuplicateRegistrations) {
-        this.allowDuplicateRegistrations = allowDuplicateRegistrations;
-
-        return this;
-    }
-
-    /** {@inheritDoc} */
-    @Override public TcpDiscoveryZookeeperIpFinder setShared(boolean shared) {
-        super.setShared(shared);
-
-        return this;
-    }
-
-    /**
-     * Empty DTO for storing service instances details. Currently acting as a placeholder because Curator requires a
-     * payload type when registering and discovering nodes. May be enhanced in the future with further information to
-     * assist discovery.
-     *
-     * @author Raul Kripalani
-     */
-    @JsonRootName("ignite_instance_details")
-    private class IgniteInstanceDetails {
-
-    }
-
-}
diff --git a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/package-info.java b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/package-info.java
deleted file mode 100644
index fcfc7b9..0000000
--- a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/package-info.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- *  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 TCP Discovery IP Finder uses Apache ZooKeeper (ZK) to locate peer nodes.
- */
-
-package org.apache.ignite.spi.discovery.tcp.ipfinder.zk;
diff --git a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTest.java b/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTest.java
deleted file mode 100644
index 897c045..0000000
--- a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/tcp/ipfinder/zk/ZookeeperIpFinderTest.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * 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.spi.discovery.tcp.ipfinder.zk;
-
-import java.util.Collection;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.apache.curator.framework.CuratorFramework;
-import org.apache.curator.framework.CuratorFrameworkFactory;
-import org.apache.curator.retry.ExponentialBackoffRetry;
-import org.apache.curator.retry.RetryNTimes;
-import org.apache.curator.test.InstanceSpec;
-import org.apache.curator.test.TestingCluster;
-import org.apache.curator.utils.CloseableUtils;
-import org.apache.ignite.Ignite;
-import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.events.Event;
-import org.apache.ignite.events.EventType;
-import org.apache.ignite.lang.IgniteBiPredicate;
-import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
-import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.WithSystemProperty;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.Timeout;
-
-/**
- * Test for {@link TcpDiscoveryZookeeperIpFinder}.
- *
- * @author Raul Kripalani
- */
-@WithSystemProperty(key = "zookeeper.jmx.log4j.disable", value = "true") // disable JMX for tests
-public class ZookeeperIpFinderTest extends GridCommonAbstractTest {
-    /** Per test timeout */
-    @Rule
-    public Timeout globalTimeout = new Timeout((int)GridTestUtils.DFLT_TEST_TIMEOUT);
-
-    /** ZK Cluster size. */
-    private static final int ZK_CLUSTER_SIZE = 3;
-
-    /** ZK Path size. */
-    private static final String SERVICES_IGNITE_ZK_PATH = "/services/ignite";
-
-    /** The ZK cluster instance, from curator-test. */
-    private TestingCluster zkCluster;
-
-    /** A Curator client to perform assertions on the embedded ZK instances. */
-    private CuratorFramework zkCurator;
-
-    /** Whether to allow duplicate registrations for the current test method or not. */
-    private boolean allowDuplicateRegistrations = false;
-
-    /** Constructor that does not start any grids. */
-    public ZookeeperIpFinderTest() {
-        super(false);
-    }
-
-    /**
-     * Before test.
-     *
-     * @throws Exception
-     */
-    @Override public void beforeTest() throws Exception {
-        super.beforeTest();
-
-        // remove stale system properties
-        System.getProperties().remove(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING);
-
-        // start the ZK cluster
-        zkCluster = new TestingCluster(ZK_CLUSTER_SIZE);
-
-        zkCluster.start();
-
-        // start the Curator client so we can perform assertions on the ZK state later
-        zkCurator = CuratorFrameworkFactory.newClient(zkCluster.getConnectString(), new RetryNTimes(10, 1000));
-        zkCurator.start();
-    }
-
-    /**
-     * After test.
-     *
-     * @throws Exception
-     */
-    @Override public void afterTest() throws Exception {
-        super.afterTest();
-
-        if (zkCurator != null)
-            CloseableUtils.closeQuietly(zkCurator);
-
-        if (zkCluster != null)
-            CloseableUtils.closeQuietly(zkCluster);
-
-        stopAllGrids();
-    }
-
-    /**
-     * Enhances the default configuration with the {#TcpDiscoveryZookeeperIpFinder}.
-     *
-     * @param igniteInstanceName Ignite instance name.
-     * @return Ignite configuration.
-     * @throws Exception If failed.
-     */
-    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
-        IgniteConfiguration configuration = super.getConfiguration(igniteInstanceName);
-
-        TcpDiscoverySpi tcpDisco = (TcpDiscoverySpi)configuration.getDiscoverySpi();
-        TcpDiscoveryZookeeperIpFinder zkIpFinder = new TcpDiscoveryZookeeperIpFinder();
-        zkIpFinder.setAllowDuplicateRegistrations(allowDuplicateRegistrations);
-
-        // first node => configure with zkUrl; second node => configure with CuratorFramework; third and subsequent
-        // shall be configured through system property
-        if (igniteInstanceName.equals(getTestIgniteInstanceName(0)))
-            zkIpFinder.setZkConnectionString(zkCluster.getConnectString());
-
-        else if (igniteInstanceName.equals(getTestIgniteInstanceName(1))) {
-            zkIpFinder.setCurator(CuratorFrameworkFactory.newClient(zkCluster.getConnectString(),
-                new ExponentialBackoffRetry(100, 5)));
-        }
-
-        tcpDisco.setIpFinder(zkIpFinder);
-
-        return configuration;
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testOneIgniteNodeIsAlone() throws Exception {
-        startGrid(0);
-
-        assertEquals(1, grid(0).cluster().metrics().getTotalNodes());
-
-        stopAllGrids();
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testTwoIgniteNodesFindEachOther() throws Exception {
-        // start one node
-        startGrid(0);
-
-        // set up an event listener to expect one NODE_JOINED event
-        CountDownLatch latch = expectJoinEvents(grid(0), 1);
-
-        // start the other node
-        startGrid(1);
-
-        // assert the nodes see each other
-        assertEquals(2, grid(0).cluster().metrics().getTotalNodes());
-        assertEquals(2, grid(1).cluster().metrics().getTotalNodes());
-
-        // assert the event listener got as many events as expected
-        latch.await(1, TimeUnit.SECONDS);
-
-        stopAllGrids();
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testThreeNodesWithThreeDifferentConfigMethods() throws Exception {
-        // start one node
-        startGrid(0);
-
-        // set up an event listener to expect one NODE_JOINED event
-        CountDownLatch latch = expectJoinEvents(grid(0), 2);
-
-        // start the 2nd node
-        startGrid(1);
-
-        // start the 3rd node, first setting the system property
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrid(2);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 2);
-
-        // assert the nodes see each other
-        assertEquals(3, grid(0).cluster().metrics().getTotalNodes());
-        assertEquals(3, grid(1).cluster().metrics().getTotalNodes());
-        assertEquals(3, grid(2).cluster().metrics().getTotalNodes());
-
-        // assert the event listener got as many events as expected
-        latch.await(1, TimeUnit.SECONDS);
-
-        stopAllGrids();
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testFourNodesStartingAndStopping() throws Exception {
-        // start one node
-        startGrid(0);
-
-        // set up an event listener to expect one NODE_JOINED event
-        CountDownLatch latch = expectJoinEvents(grid(0), 3);
-
-        // start the 2nd node
-        startGrid(1);
-
-        // start the 3rd & 4th nodes, first setting the system property
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrid(2);
-        startGrid(3);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 3);
-
-        // assert the nodes see each other
-        assertEquals(4, grid(0).cluster().metrics().getTotalNodes());
-        assertEquals(4, grid(1).cluster().metrics().getTotalNodes());
-        assertEquals(4, grid(2).cluster().metrics().getTotalNodes());
-        assertEquals(4, grid(3).cluster().metrics().getTotalNodes());
-
-        // assert the event listener got as many events as expected
-        latch.await(1, TimeUnit.SECONDS);
-
-        // stop the first grid
-        stopGrid(0);
-
-        // make sure that nodes were synchronized; they should only see 3 now
-        assertEquals(3, grid(1).cluster().metrics().getTotalNodes());
-        assertEquals(3, grid(2).cluster().metrics().getTotalNodes());
-        assertEquals(3, grid(3).cluster().metrics().getTotalNodes());
-
-        // stop all remaining grids
-        stopGrid(1);
-        stopGrid(2);
-        stopGrid(3);
-
-        // check that the nodes are gone in ZK
-        assertEquals(0, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testFourNodesWithDuplicateRegistrations() throws Exception {
-        allowDuplicateRegistrations = true;
-
-        // start 4 nodes
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrids(4);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 3);
-
-        // each node will register itself + the node that it connected to to join the cluster
-        assertEquals(7, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-        // stop all grids
-        stopAllGrids();
-
-        // check that all nodes are gone in ZK
-        assertEquals(0, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testFourNodesWithNoDuplicateRegistrations() throws Exception {
-        allowDuplicateRegistrations = false;
-
-        // start 4 nodes
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrids(4);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 3);
-
-        // each node will only register itself
-        assertEquals(4, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-        // stop all grids
-        stopAllGrids();
-
-        // check that all nodes are gone in ZK
-        assertEquals(0, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testFourNodesRestartLastSeveralTimes() throws Exception {
-        allowDuplicateRegistrations = false;
-
-        // start 4 nodes
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrids(4);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 3);
-
-        // each node will only register itself
-        assertEquals(4, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-        // repeat 5 times
-        for (int i = 0; i < 5; i++) {
-            // stop last grid
-            stopGrid(2);
-
-            // check that the node has unregistered itself and its party
-            assertEquals(3, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-            // start the node again
-            startGrid(2);
-
-            // check that the node back in ZK
-            assertEquals(4, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-        }
-
-        stopAllGrids();
-
-        assertEquals(0, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-    }
-
-    /**
-     * @throws Exception If failed.
-     */
-    @Test
-    public void testFourNodesKillRestartZookeeper() throws Exception {
-        allowDuplicateRegistrations = false;
-
-        // start 4 nodes
-        System.setProperty(TcpDiscoveryZookeeperIpFinder.PROP_ZK_CONNECTION_STRING, zkCluster.getConnectString());
-        startGrids(4);
-
-        // wait until all grids are started
-        waitForRemoteNodes(grid(0), 3);
-
-        // each node will only register itself
-        assertEquals(4, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-        // remember ZK server configuration and stop the cluster
-        Collection<InstanceSpec> instances = zkCluster.getInstances();
-        zkCluster.stop();
-        Thread.sleep(1000);
-
-        // start the cluster with the previous configuration
-        zkCluster = new TestingCluster(instances);
-        zkCluster.start();
-
-        // block the client until connected
-        zkCurator.blockUntilConnected();
-
-        // Check that the nodes have registered again with the previous configuration.
-        assertEquals(4, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-
-        // Block the clients until connected.
-        for (int i = 0; i < 4; i++) {
-            TcpDiscoverySpi spi = (TcpDiscoverySpi)grid(i).configuration().getDiscoverySpi();
-
-            TcpDiscoveryZookeeperIpFinder zkIpFinder = (TcpDiscoveryZookeeperIpFinder)spi.getIpFinder();
-
-            CuratorFramework curator = GridTestUtils.getFieldValue(zkIpFinder, "curator");
-
-            curator.blockUntilConnected();
-        }
-
-        // stop all grids
-        stopAllGrids();
-
-        assertEquals(0, zkCurator.getChildren().forPath(SERVICES_IGNITE_ZK_PATH).size());
-    }
-
-    /**
-     * @param ignite Node.
-     * @param joinEvtCnt Expected events number.
-     * @return Events latch.
-     */
-    private CountDownLatch expectJoinEvents(Ignite ignite, int joinEvtCnt) {
-        final CountDownLatch latch = new CountDownLatch(joinEvtCnt);
-
-        ignite.events().remoteListen(new IgniteBiPredicate<UUID, Event>() {
-            @Override public boolean apply(UUID uuid, Event evt) {
-                latch.countDown();
-                return true;
-            }
-        }, null, EventType.EVT_NODE_JOINED);
-
-        return latch;
-    }
-}
diff --git a/packaging/deb/changelog b/packaging/deb/changelog
index fef1c77..a808d9e 100644
--- a/packaging/deb/changelog
+++ b/packaging/deb/changelog
@@ -1,3 +1,9 @@
+apache-ignite (2.12.0-1) unstable; urgency=low
+
+  * Updated Apache Ignite to version 2.12.0
+
+ -- Nikita Amelchev <namelchev@apache.org>  Thu, 21 Oct 2021 22:00:00 +0300
+
 apache-ignite (2.11.0-1) unstable; urgency=low
 
   * Updated Apache Ignite to version 2.11.0
diff --git a/packaging/rpm/apache-ignite.spec b/packaging/rpm/apache-ignite.spec
index 7afd9b8..e438fb1 100644
--- a/packaging/rpm/apache-ignite.spec
+++ b/packaging/rpm/apache-ignite.spec
@@ -12,7 +12,7 @@
 #
 
 Name:             apache-ignite
-Version:          2.11.0
+Version:          2.12.0
 Release:          1
 Summary:          Apache Ignite In-Memory Computing, Database and Caching Platform
 Group:            Development/System
@@ -267,6 +267,9 @@
 # Changelog
 #
 
+* Thu Oct 21 2021 Nikita Amelchev <namelchev@apache.org> - 2.12.0-1
+- Updated Apache Ignite to version 2.12.0
+
 * Fri Jul 09 2021 Alexey Gidaspov <olive.crow@gmail.com> - 2.11.0-1
 - Updated Apache Ignite to version 2.11.0
 
diff --git a/parent/pom.xml b/parent/pom.xml
index 4080cd5..41183ba 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -78,7 +78,6 @@
         <httpclient.version>4.5.13</httpclient.version>
         <httpcore.version>4.4.14</httpcore.version>
         <jackson.version>2.12.4</jackson.version>
-        <jackson1.version>1.9.13</jackson1.version>
         <jaxb.api.version>2.1</jaxb.api.version>
         <jaxb.impl.version>2.1.14</jaxb.impl.version>
         <javassist.version>3.20.0-GA</javassist.version>
@@ -100,12 +99,13 @@
         <jtidy.version>r938</jtidy.version>
         <kafka.version>2.0.1</kafka.version>
         <karaf.version>4.0.2</karaf.version>
-        <log4j.version>2.11.0</log4j.version>
+        <log4j.version>2.17.1</log4j.version>
         <lucene.bundle.version>7.4.0_1</lucene.bundle.version>
         <lucene.version>7.4.0</lucene.version>
         <lz4.version>1.5.0</lz4.version>
         <maven.bundle.plugin.version>3.5.0</maven.bundle.plugin.version>
         <maven.checkstyle.plugin.version>3.1.1</maven.checkstyle.plugin.version>
+        <maven.model.version>3.8.4</maven.model.version>
         <checkstyle.puppycrawl.version>8.45</checkstyle.puppycrawl.version>
         <mockito.version>3.4.6</mockito.version>
         <mysql.connector.version>8.0.26</mysql.connector.version>
@@ -119,7 +119,7 @@
         <scala210.library.version>2.10.7</scala210.library.version>
         <scala.library.version>2.11.12</scala.library.version>
         <scala.test.version>2.2.6</scala.test.version>
-        <slf4j.version>1.7.7</slf4j.version>
+        <slf4j.version>1.7.33</slf4j.version>
         <slf4j16.version>1.6.4</slf4j16.version>
         <snappy.version>1.1.7.2</snappy.version>
         <spark.hadoop.version>2.6.5</spark.hadoop.version>
@@ -366,6 +366,10 @@
                                 <packages>org.apache.ignite.mbean:org.apache.ignite.mxbean</packages>
                             </group>
                             <group>
+                                <title>Memory allocator APIs</title>
+                                <packages>org.apache.ignite.mem</packages>
+                            </group>
+                            <group>
                                 <title>SPI: CheckPoint</title>
                                 <packages>org.apache.ignite.spi.checkpoint*</packages>
                             </group>
diff --git a/pom.xml b/pom.xml
index f93a4c3..715cf1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,7 @@
                 <module>modules/hibernate-4.2</module>
                 <module>modules/hibernate-5.1</module>
                 <module>modules/hibernate-5.3</module>
+                <module>modules/numa-allocator</module>
                 <module>modules/schedule</module>
                 <module>modules/yardstick</module>
             </modules>
@@ -131,6 +132,13 @@
         </profile>
 
         <profile>
+            <id>numa-allocator</id>
+            <modules>
+                <module>modules/numa-allocator</module>
+            </modules>
+        </profile>
+
+        <profile>
             <id>test</id>
             <build>
                 <plugins>