You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by tl...@apache.org on 2020/06/26 10:28:46 UTC

[ignite] branch master updated: IGNITE-13154 Add command line commands (control.sh) to manage binary types (#7936)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b6bd5a3  IGNITE-13154 Add command line commands (control.sh) to manage binary types (#7936)
b6bd5a3 is described below

commit b6bd5a3855bb2ad4eb3d8791bd707134cc7d2b28
Author: tledkov <tl...@gridgain.com>
AuthorDate: Fri Jun 26 13:28:30 2020 +0300

    IGNITE-13154 Add command line commands (control.sh) to manage binary types (#7936)
---
 .../org/apache/ignite/internal/IgniteFeatures.java |   4 +-
 .../ignite/internal/binary/BinaryContext.java      |   7 +
 .../apache/ignite/internal/binary/BinaryUtils.java |   7 +-
 .../internal/client/thin/ReliableChannel.java      |  12 +
 .../internal/client/thin/TcpIgniteClient.java      |  18 +-
 .../internal/commandline/BaselineCommand.java      |   4 +-
 .../internal/commandline/CommandArgIterator.java   |  41 +-
 .../ignite/internal/commandline/CommandList.java   |   6 +-
 .../internal/commandline/CommonArgParser.java      |   4 +-
 .../ignite/internal/commandline/TxCommands.java    |   6 +-
 .../internal/commandline/meta/MetadataCommand.java | 132 ++++++
 .../commandline/meta/MetadataSubCommandsList.java  |  78 ++++
 .../subcommands/MetadataAbstractSubCommand.java    | 145 +++++++
 .../meta/subcommands/MetadataDetailsCommand.java   |  90 +++++
 .../meta/subcommands/MetadataHelpCommand.java      |  54 +++
 .../meta/subcommands/MetadataListCommand.java      |  56 +++
 .../meta/subcommands/MetadataRemoveCommand.java    | 116 ++++++
 .../meta/subcommands/MetadataUpdateCommand.java    |  88 ++++
 .../commandline/meta/tasks/MetadataInfoTask.java   |  91 +++++
 .../commandline/meta/tasks/MetadataListResult.java |  74 ++++
 .../commandline/meta/tasks/MetadataMarshalled.java |  86 ++++
 .../commandline/meta/tasks/MetadataRemoveTask.java | 149 +++++++
 .../commandline/meta/tasks/MetadataTypeArgs.java   | 129 ++++++
 .../commandline/meta/tasks/MetadataUpdateTask.java |  99 +++++
 .../internal/jdbc/thin/JdbcThinConnection.java     |   8 +-
 .../cache/binary/BinaryMetadataFileStore.java      | 204 ++++++++--
 .../cache/binary/BinaryMetadataHolder.java         |  32 +-
 .../cache/binary/BinaryMetadataTransport.java      | 446 +++++++++++++++------
 .../binary/CacheObjectBinaryProcessorImpl.java     |  82 +++-
 .../binary/MetadataRemoveAcceptedMessage.java      |  96 +++++
 .../binary/MetadataRemoveProposedMessage.java      | 143 +++++++
 .../cacheobject/IgniteCacheObjectProcessor.java    |  10 +
 .../processors/odbc/ClientListenerProcessor.java   | 138 ++++---
 .../ignite/plugin/security/SecurityPermission.java |   5 +-
 .../main/resources/META-INF/classnames.properties  |  11 +
 .../commandline/CommandHandlerParsingTest.java     |   3 +-
 .../cache/binary/BinaryMetadataRemoveTest.java     | 308 ++++++++++++++
 .../BinaryMetadataRemoveWithPersistenceTest.java   |  86 ++++
 .../IgnitePdsBinaryMetadataAsyncWritingTest.java   |   2 +-
 .../testsuites/IgniteBinaryObjectsTestSuite.java   |   5 +
 .../util/GridCommandHandlerClusterByClassTest.java |   8 +-
 ...ridCommandHandlerClusterByClassTest_help.output |  16 +
 ...andHandlerClusterByClassWithSSLTest_help.output |  16 +
 ...teCacheWithIndexingAndPersistenceTestSuite.java |   4 +-
 .../util/GridCommandHandlerMetadataTest.java       | 384 ++++++++++++++++++
 45 files changed, 3260 insertions(+), 243 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteFeatures.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteFeatures.java
index 904d93e..d245459 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteFeatures.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteFeatures.java
@@ -105,8 +105,10 @@ public enum IgniteFeatures {
     CHECK_INDEX_INLINE_SIZES(36),
 
     /** Distributed propagation of tx collisions dump interval. */
-    DISTRIBUTED_TX_COLLISIONS_DUMP(37);
+    DISTRIBUTED_TX_COLLISIONS_DUMP(37),
 
+    /** Remove metadata from cluster for specified type. */
+    REMOVE_METADATA(39);
     /**
      * Unique feature identifier.
      */
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 6379174..52dcf1c 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
@@ -1450,6 +1450,13 @@ public class BinaryContext {
     }
 
     /**
+     * @param typeId Type ID.
+     */
+    public synchronized void removeType(int typeId) {
+        schemas.remove(typeId);
+    }
+
+    /**
      * Type descriptors.
      */
     private static class TypeDescriptors {
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 ed2b3ac..8491943 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
@@ -1044,8 +1044,11 @@ public class BinaryUtils {
                             "Type '" + oldMeta.typeName() + "' with typeId " + oldMeta.typeId()
                                 + " has a different/incorrect type for field '" + newField.getKey()
                                 + "'. Expected '" + oldFieldTypeName + "' but '" + newFieldTypeName
-                                + "' was provided. Field type's modification is unsupported, clean {root_path}/marshaller " +
-                                "and {root_path}/db/binary_meta directories if the type change is required."
+                                + "' was provided. The type of an existing field can not be changed. " +
+                                "Use a different field name or follow this procedure to reuse the current name:\n" +
+                                "- Delete data records that use the old field type;\n" +
+                                "- Remove metadata by the command: " +
+                                "'control.sh --meta remove --typeId " + oldMeta.typeId() + "'."
                         );
                     }
                 }
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 89cc783..a2c81d1 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
@@ -104,6 +104,9 @@ final class ReliableChannel implements AutoCloseable, NotificationListener {
     /** Channel is closed. */
     private volatile boolean closed;
 
+    /** Fail (disconnect) listeners. */
+    private ArrayList<Runnable> chFailLsnrs = new ArrayList<>();
+
     /**
      * Constructor.
      */
@@ -410,6 +413,8 @@ final class ReliableChannel implements AutoCloseable, NotificationListener {
         // when current index was changed and no other wrong channel will be closed by current thread because
         // onChannelFailure checks channel binded to the holder before closing it.
         onChannelFailure(channels[curChIdx], ch);
+
+        chFailLsnrs.forEach(Runnable::run);
     }
 
     /**
@@ -462,6 +467,13 @@ final class ReliableChannel implements AutoCloseable, NotificationListener {
     }
 
     /**
+     * @param chFailLsnr Listener for the channel fail (disconnect).
+     */
+    public void addChannelFailListener(Runnable chFailLsnr) {
+        chFailLsnrs.add(chFailLsnr);
+    }
+
+    /**
      * Channels holder.
      */
     private class ClientChannelHolder {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
index 3db66f5..b5ab5e4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
@@ -103,7 +103,9 @@ public class TcpIgniteClient implements IgniteClient {
         Function<ClientChannelConfiguration, ClientChannel> chFactory,
         ClientConfiguration cfg
     ) throws ClientException {
-        marsh = new ClientBinaryMarshaller(new ClientBinaryMetadataHandler(), new ClientMarshallerContext());
+        final ClientBinaryMetadataHandler metadataHandler = new ClientBinaryMetadataHandler();
+
+        marsh = new ClientBinaryMarshaller(metadataHandler, new ClientMarshallerContext());
 
         marsh.setBinaryConfiguration(cfg.getBinaryConfiguration());
 
@@ -113,6 +115,8 @@ public class TcpIgniteClient implements IgniteClient {
 
         ch = new ReliableChannel(chFactory, cfg, binary);
 
+        ch.addChannelFailListener(() -> metadataHandler.onReconnect());
+
         transactions = new TcpClientTransactions(ch, marsh,
             new ClientTransactionConfiguration(cfg.getTransactionConfiguration()));
 
@@ -292,10 +296,11 @@ public class TcpIgniteClient implements IgniteClient {
      */
     private class ClientBinaryMetadataHandler implements BinaryMetadataHandler {
         /** In-memory metadata cache. */
-        private final BinaryMetadataHandler cache = BinaryCachingMetadataHandler.create();
+        private volatile BinaryMetadataHandler cache = BinaryCachingMetadataHandler.create();
 
         /** {@inheritDoc} */
-        @Override public void addMeta(int typeId, BinaryType meta, boolean failIfUnregistered) throws BinaryObjectException {
+        @Override public void addMeta(int typeId, BinaryType meta, boolean failIfUnregistered)
+            throws BinaryObjectException {
             if (cache.metadata(typeId) == null) {
                 try {
                     ch.request(
@@ -372,6 +377,13 @@ public class TcpIgniteClient implements IgniteClient {
         @Override public Collection<BinaryType> metadata() throws BinaryObjectException {
             return cache.metadata();
         }
+
+        /**
+         * Clear local cache on reconnect.
+         */
+        void onReconnect() {
+            cache = BinaryCachingMetadataHandler.create();
+        }
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/BaselineCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/BaselineCommand.java
index 46d4f6e..6991dcb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/commandline/BaselineCommand.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/BaselineCommand.java
@@ -247,7 +247,7 @@ public class BaselineCommand implements Command<BaselineArguments> {
                 break;
 
             case VERSION:
-                baselineArgs.withTopVer(argIter.nextLongArg("topology version"));
+                baselineArgs.withTopVer(argIter.nextNonNegativeLongArg("topology version"));
 
                 break;
 
@@ -264,7 +264,7 @@ public class BaselineCommand implements Command<BaselineArguments> {
                         baselineArgs.withEnable(autoAdjustArg == AutoAdjustCommandArg.ENABLE);
 
                     if (autoAdjustArg == AutoAdjustCommandArg.TIMEOUT)
-                        baselineArgs.withSoftBaselineTimeout(argIter.nextLongArg("soft timeout"));
+                        baselineArgs.withSoftBaselineTimeout(argIter.nextNonNegativeLongArg("soft timeout"));
                 }
                 while (argIter.hasNextSubArg());
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandArgIterator.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandArgIterator.java
index d9ff51c..0e1e89b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandArgIterator.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandArgIterator.java
@@ -100,16 +100,49 @@ public class CommandArgIterator {
     /**
      * @return Numeric value.
      */
+    public long nextNonNegativeLongArg(String argName) {
+        long val = nextLongArg(argName);
+
+        if (val < 0)
+            throw new IllegalArgumentException("Invalid value for " + argName + ": " + val);
+
+        return val;
+    }
+
+    /**
+     * @return Numeric value.
+     */
+    public int nextNonNegativeIntArg(String argName) {
+        int val = nextIntArg(argName);
+
+        if (val < 0)
+            throw new IllegalArgumentException("Invalid value for " + argName + ": " + val);
+
+        return val;
+    }
+
+    /**
+     * @return Numeric value.
+     */
     public long nextLongArg(String argName) {
         String str = nextArg("Expecting " + argName);
 
         try {
-            long val = Long.parseLong(str);
+            return str.startsWith("0x") ? Long.parseLong(str.substring(2), 16) : Long.parseLong(str);
+        }
+        catch (NumberFormatException ignored) {
+            throw new IllegalArgumentException("Invalid value for " + argName + ": " + str);
+        }
+    }
 
-            if (val < 0)
-                throw new IllegalArgumentException("Invalid value for " + argName + ": " + val);
+    /**
+     * @return Numeric value.
+     */
+    public int nextIntArg(String argName) {
+        String str = nextArg("Expecting " + argName);
 
-            return val;
+        try {
+            return str.startsWith("0x") ? Integer.parseInt(str.substring(2), 16) : Integer.parseInt(str);
         }
         catch (NumberFormatException ignored) {
             throw new IllegalArgumentException("Invalid value for " + argName + ": " + str);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandList.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
index 196ad4d..a86c06d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.commandline;
 import org.apache.ignite.internal.commandline.cache.CacheCommands;
 import org.apache.ignite.internal.commandline.diagnostic.DiagnosticCommand;
 import org.apache.ignite.internal.commandline.encryption.EncryptionCommand;
+import org.apache.ignite.internal.commandline.meta.MetadataCommand;
 import org.apache.ignite.internal.commandline.query.KillCommand;
 import org.apache.ignite.internal.commandline.snapshot.SnapshotCommand;
 
@@ -61,7 +62,10 @@ public enum CommandList {
     KILL("--kill", new KillCommand()),
 
     /** Snapshot commands. */
-    SNAPSHOT("--snapshot", new SnapshotCommand());
+    SNAPSHOT("--snapshot", new SnapshotCommand()),
+
+    /** Metadata commands. */
+    METADATA("--meta", new MetadataCommand());
 
     /** Private values copy so there's no need in cloning it every time. */
     private static final CommandList[] VALUES = CommandList.values();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommonArgParser.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommonArgParser.java
index 88b9d73..5d3568f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommonArgParser.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/CommonArgParser.java
@@ -242,12 +242,12 @@ public class CommonArgParser {
                         break;
 
                     case CMD_PING_INTERVAL:
-                        pingInterval = argIter.nextLongArg("ping interval");
+                        pingInterval = argIter.nextNonNegativeLongArg("ping interval");
 
                         break;
 
                     case CMD_PING_TIMEOUT:
-                        pingTimeout = argIter.nextLongArg("ping timeout");
+                        pingTimeout = argIter.nextNonNegativeLongArg("ping timeout");
 
                         break;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/TxCommands.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/TxCommands.java
index 6b0d10f..fbe89f2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/commandline/TxCommands.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/TxCommands.java
@@ -233,7 +233,7 @@ public class TxCommands implements Command<VisorTxTaskArg> {
                 case TX_LIMIT:
                     argIter.nextArg("");
 
-                    limit = (int)argIter.nextLongArg(TxCommandArg.TX_LIMIT.toString());
+                    limit = (int)argIter.nextNonNegativeLongArg(TxCommandArg.TX_LIMIT.toString());
 
                     break;
 
@@ -271,13 +271,13 @@ public class TxCommands implements Command<VisorTxTaskArg> {
                 case TX_DURATION:
                     argIter.nextArg("");
 
-                    duration = argIter.nextLongArg(TxCommandArg.TX_DURATION.toString()) * 1000L;
+                    duration = argIter.nextNonNegativeLongArg(TxCommandArg.TX_DURATION.toString()) * 1000L;
                     break;
 
                 case TX_SIZE:
                     argIter.nextArg("");
 
-                    size = (int)argIter.nextLongArg(TxCommandArg.TX_SIZE.toString());
+                    size = (int)argIter.nextNonNegativeLongArg(TxCommandArg.TX_SIZE.toString());
                     break;
 
                 case TX_LABEL:
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataCommand.java
new file mode 100644
index 0000000..bdf53b5
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataCommand.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataRemoveCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataUpdateCommand;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataTypeArgs;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_ENABLE_EXPERIMENTAL_COMMAND;
+import static org.apache.ignite.internal.commandline.Command.usage;
+import static org.apache.ignite.internal.commandline.CommandHandler.UTILITY_NAME;
+import static org.apache.ignite.internal.commandline.CommandList.METADATA;
+import static org.apache.ignite.internal.commandline.CommandLogger.optional;
+import static org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList.DETAILS;
+import static org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList.HELP;
+import static org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList.LIST;
+import static org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList.REMOVE;
+import static org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList.UPDATE;
+
+/**
+ *
+ */
+public class MetadataCommand implements Command<Object> {
+    /**
+     *
+     */
+    private Command<?> delegate;
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        if (!experimentalEnabled())
+            return;
+
+        usage(log, "Print metadata command help:",
+            METADATA,
+            HELP.toString()
+        );
+
+        usage(log, "Print list of binary metadata types:",
+            METADATA,
+            LIST.toString()
+        );
+
+        usage(log, "Print detailed info about specified binary type " +
+                "(the type must be specified by type name or by type identifier):",
+            METADATA,
+            DETAILS.toString(),
+            optional(MetadataTypeArgs.TYPE_ID, "<typeId>"),
+            optional(MetadataTypeArgs.TYPE_NAME, "<typeName>")
+        );
+
+        usage(log, "Remove the metadata of the specified type " +
+                "(the type must be specified by type name or by type identifier) from cluster and saves the removed " +
+                "metadata to the specified file. \n" +
+                "If the file name isn't specified the output file name is: '<typeId>.bin'",
+            METADATA,
+            REMOVE.toString(),
+            optional(MetadataTypeArgs.TYPE_ID, "<typeId>"),
+            optional(MetadataTypeArgs.TYPE_NAME, "<typeName>"),
+            optional(MetadataRemoveCommand.OUT_FILE_NAME, "<fileName>")
+        );
+
+        usage(log, "Update cluster metadata from specified file (file name is required)",
+            METADATA,
+            UPDATE.toString(),
+            MetadataUpdateCommand.IN_FILE_NAME, "<fileName>"
+        );
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return METADATA.toCommandName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        MetadataSubCommandsList subcommand = MetadataSubCommandsList.parse(argIter.nextArg("Expected metadata action."));
+
+        if (subcommand == null)
+            throw new IllegalArgumentException("Expected correct metadata action.");
+
+        delegate = subcommand.command();
+
+        delegate.parseArguments(argIter);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean experimental() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String confirmationPrompt() {
+        return delegate != null ? delegate.confirmationPrompt() : null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        if (experimentalEnabled())
+            return delegate.execute(clientCfg, log);
+        else {
+            log.warning(String.format("For use experimental command add %s=true to JVM_OPTS in %s",
+                IGNITE_ENABLE_EXPERIMENTAL_COMMAND, UTILITY_NAME));
+
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object arg() {
+        return delegate.arg();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataSubCommandsList.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataSubCommandsList.java
new file mode 100644
index 0000000..b111984
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/MetadataSubCommandsList.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.internal.commandline.meta;
+
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataDetailsCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataHelpCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataListCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataRemoveCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataUpdateCommand;
+import org.jetbrains.annotations.NotNull;
+
+/** */
+public enum MetadataSubCommandsList {
+    /** */
+    HELP("help", new MetadataHelpCommand()),
+    /** */
+    LIST("list", new MetadataListCommand()),
+    /** */
+    DETAILS("details", new MetadataDetailsCommand()),
+    /** */
+    REMOVE("remove", new MetadataRemoveCommand()),
+    /** */
+    UPDATE("update", new MetadataUpdateCommand());
+
+    /** */
+    private final String name;
+
+    /** */
+    private final Command<?> cmd;
+
+    /** */
+    MetadataSubCommandsList(String name, Command<?> cmd) {
+        this.name = name;
+        this.cmd = cmd;
+    }
+
+    /** */
+    public String text() {
+        return name;
+    }
+
+    /** */
+    @NotNull
+    public Command<?> command() {
+        return cmd;
+    }
+
+    /** */
+    public static MetadataSubCommandsList parse(String name) {
+        for (MetadataSubCommandsList cmd : values()) {
+            if (cmd.name.equalsIgnoreCase(name))
+                return cmd;
+        }
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return name;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataAbstractSubCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataAbstractSubCommand.java
new file mode 100644
index 0000000..0e586e0
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataAbstractSubCommand.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta.subcommands;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.util.Collection;
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientCompute;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.client.GridClientDisconnectedException;
+import org.apache.ignite.internal.client.GridClientNode;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.visor.VisorTaskArgument;
+
+/** */
+public abstract class MetadataAbstractSubCommand<
+    MetadataArgsDto extends IgniteDataTransferObject,
+    MetadataResultDto extends IgniteDataTransferObject>
+    implements Command<MetadataArgsDto> {
+    /** Filesystem. */
+    protected static final FileSystem FS = FileSystems.getDefault();
+
+    /** */
+    private MetadataArgsDto args;
+
+    /** {@inheritDoc} */
+    @Override public boolean experimental() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final void printUsage(Logger log) {
+        throw new UnsupportedOperationException("printUsage");
+    }
+
+    /** {@inheritDoc} */
+    @Override public final void parseArguments(CommandArgIterator argIter) {
+        args = parseArguments0(argIter);
+    }
+
+    /** {@inheritDoc} */
+    @Override public final Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            GridClientCompute compute = client.compute();
+
+            // Try to find connectable server nodes.
+            Collection<GridClientNode> nodes = compute.nodes((n) -> n.connectable() && !n.isClient());
+
+            if (F.isEmpty(nodes)) {
+                nodes = compute.nodes(GridClientNode::connectable);
+
+                if (F.isEmpty(nodes))
+                    throw new GridClientDisconnectedException("Connectable nodes not found", null);
+            }
+
+            GridClientNode node = nodes.stream()
+                .findAny().orElse(null);
+
+            if (node == null)
+                node = compute.balancer().balancedNode(nodes);
+
+            MetadataResultDto res = compute.projection(node).execute(
+                taskName(),
+                new VisorTaskArgument<>(node.nodeId(), arg(), false)
+            );
+
+            printResult(res, log);
+        }
+        catch (Throwable e) {
+            log.severe("Failed to execute metadata command='" + name() + "'");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final MetadataArgsDto arg() {
+        return args;
+    }
+
+    /** */
+    protected abstract String taskName();
+
+    /** */
+    protected MetadataArgsDto parseArguments0(CommandArgIterator argIter) {
+        return null;
+    }
+
+    /** */
+    protected abstract void printResult(MetadataResultDto res, Logger log);
+
+    /**
+     * @param val Integer value.
+     * @return String.
+     */
+    protected String printInt(int val) {
+        return "0x" + Integer.toHexString(val).toUpperCase() + " (" + val + ')';
+    }
+
+    /**
+     *
+     */
+    public static class VoidDto extends IgniteDataTransferObject {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte protoVer, ObjectInput in)
+            throws IOException, ClassNotFoundException {
+            // No-op.
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataDetailsCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataDetailsCommand.java
new file mode 100644
index 0000000..4c919c0
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataDetailsCommand.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta.subcommands;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataInfoTask;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataListResult;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataTypeArgs;
+import org.apache.ignite.internal.util.typedef.F;
+
+import static org.apache.ignite.internal.commandline.CommandLogger.INDENT;
+
+/** */
+public class MetadataDetailsCommand
+    extends MetadataAbstractSubCommand<MetadataTypeArgs, MetadataListResult>
+{
+    /** {@inheritDoc} */
+    @Override protected String taskName() {
+        return MetadataInfoTask.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public MetadataTypeArgs parseArguments0(CommandArgIterator argIter) {
+        return MetadataTypeArgs.parseArguments(argIter);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void printResult(MetadataListResult res, Logger log) {
+        if (res.metadata() == null) {
+            log.info("Type not found");
+
+            return;
+        }
+
+        assert res.metadata().size() == 1 : "Unexpected  metadata results: " + res.metadata();
+
+        BinaryMetadata m = F.first(res.metadata());
+
+        log.info("typeId=" + printInt(m.typeId()));
+        log.info("typeName=" + m.typeName());
+        log.info("Fields:");
+
+        final Map<Integer, String> fldMap = new HashMap<>();
+        m.fieldsMap().forEach((name, fldMeta) -> {
+            log.info(INDENT +
+                "name=" + name +
+                ", type=" + BinaryUtils.fieldTypeName(fldMeta.typeId()) +
+                ", fieldId=" + printInt(fldMeta.fieldId()));
+
+            fldMap.put(fldMeta.fieldId(), name);
+        });
+
+        log.info("Schemas:");
+
+        m.schemas().forEach(s ->
+            log.info(INDENT +
+                "schemaId=" + printInt(s.schemaId()) +
+                ", fields=" + Arrays.stream(s.fieldIds())
+                    .mapToObj(fldMap::get)
+                    .collect(Collectors.toList())));
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return MetadataSubCommandsList.LIST.text();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataHelpCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataHelpCommand.java
new file mode 100644
index 0000000..4f63be5
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataHelpCommand.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta.subcommands;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.meta.MetadataCommand;
+import org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList;
+
+/** */
+public class MetadataHelpCommand implements Command<Void> {
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        throw new UnsupportedOperationException("printUsage");
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        new MetadataCommand().printUsage(log);
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean experimentalEnabled() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Void arg() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return MetadataSubCommandsList.HELP.text();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataListCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataListCommand.java
new file mode 100644
index 0000000..a559f12
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataListCommand.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.commandline.meta.subcommands;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataInfoTask;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataListResult;
+
+/** */
+public class MetadataListCommand
+    extends MetadataAbstractSubCommand<MetadataAbstractSubCommand.VoidDto, MetadataListResult>
+{
+    /** {@inheritDoc} */
+    @Override protected String taskName() {
+        return MetadataInfoTask.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public VoidDto parseArguments0(CommandArgIterator argIter) {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void printResult(MetadataListResult res, Logger log) {
+        for (BinaryMetadata m : res.metadata()) {
+            log.info("typeId=" + printInt(m.typeId()) +
+                ", typeName=" + m.typeName() +
+                ", fields=" + m.fields().size() +
+                ", schemas=" + m.schemas().size() +
+                ", isEnum=" + m.isEnum());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return MetadataSubCommandsList.LIST.text();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataRemoveCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataRemoveCommand.java
new file mode 100644
index 0000000..0e2929b
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataRemoveCommand.java
@@ -0,0 +1,116 @@
+/*
+ * 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.commandline.meta.subcommands;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataMarshalled;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataRemoveTask;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataTypeArgs;
+
+/**
+ *
+ */
+public class MetadataRemoveCommand
+    extends MetadataAbstractSubCommand<MetadataTypeArgs, MetadataMarshalled> {
+    /** Output file name. */
+    public static final String OUT_FILE_NAME = "--out";
+
+    /** Output file. */
+    private Path outFile;
+
+    /** {@inheritDoc} */
+    @Override protected String taskName() {
+        return MetadataRemoveTask.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String confirmationPrompt() {
+        return "Warning: the command will remove the binary metadata for a type \""
+            + arg().toString() + "\" from cluster.";
+    }
+
+    /** {@inheritDoc} */
+    @Override public MetadataTypeArgs parseArguments0(CommandArgIterator argIter) {
+        outFile = null;
+
+        MetadataTypeArgs argType = MetadataTypeArgs.parseArguments(argIter);
+
+        while (argIter.hasNextSubArg() && outFile == null) {
+            String opt = argIter.nextArg("");
+
+            if (OUT_FILE_NAME.equalsIgnoreCase(opt)) {
+                String fileName = argIter.nextArg("output file name");
+
+                outFile = FS.getPath(fileName);
+            }
+        }
+
+        if (outFile != null) {
+            try (OutputStream os = Files.newOutputStream(outFile)) {
+                os.close();
+
+                Files.delete(outFile);
+            }
+            catch (IOException e) {
+                throw new IllegalArgumentException("Cannot write to output file " + outFile +
+                    ". Error: " + e.toString(), e);
+            }
+        }
+
+        return argType;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void printResult(MetadataMarshalled res, Logger log) {
+        if (res.metadata() == null) {
+            log.info("Type not found");
+
+            return;
+        }
+
+        BinaryMetadata m = res.metadata();
+
+        if (outFile == null)
+            outFile = FS.getPath(m.typeId() + ".bin");
+
+        try (OutputStream os = Files.newOutputStream(outFile)) {
+            os.write(res.metadataMarshalled());
+        }
+        catch (IOException e) {
+            log.severe("Cannot store removed type '" + m.typeName() + "' to: " + outFile);
+            log.severe(CommandLogger.errorMessage(e));
+
+            return;
+        }
+
+        log.info("Type '" + m.typeName() + "' is removed. Metadata is stored at: " + outFile);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return MetadataSubCommandsList.REMOVE.text();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataUpdateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataUpdateCommand.java
new file mode 100644
index 0000000..5b7f46c
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/subcommands/MetadataUpdateCommand.java
@@ -0,0 +1,88 @@
+/*
+ * 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.commandline.meta.subcommands;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.meta.MetadataSubCommandsList;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataMarshalled;
+import org.apache.ignite.internal.commandline.meta.tasks.MetadataUpdateTask;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/** */
+public class MetadataUpdateCommand
+    extends MetadataAbstractSubCommand<MetadataMarshalled, MetadataMarshalled>
+{
+    /** Output file name. */
+    public static final String IN_FILE_NAME = "--in";
+
+    /** {@inheritDoc} */
+    @Override protected String taskName() {
+        return MetadataUpdateTask.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String confirmationPrompt() {
+        return "Warning: the command will update the binary metadata at the cluster.";
+    }
+
+    /** {@inheritDoc} */
+    @Override public MetadataMarshalled parseArguments0(CommandArgIterator argIter) {
+        String opt = argIter.nextArg(IN_FILE_NAME);
+
+        if (!IN_FILE_NAME.equalsIgnoreCase(opt))
+            throw new IllegalArgumentException("The option '" + IN_FILE_NAME + "' is required");
+
+        Path inFile = FS.getPath(argIter.nextArg("input file name"));
+
+        try (InputStream is = Files.newInputStream(inFile)) {
+            ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+            U.copy(is, buf);
+
+            return new MetadataMarshalled(buf.toByteArray(), null);
+        }
+        catch (IOException e) {
+            throw new IllegalArgumentException("Cannot read metadata from " + inFile, e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void printResult(MetadataMarshalled res, Logger log) {
+        if (res.metadata() == null) {
+            log.info("Type not found");
+
+            return;
+        }
+
+        BinaryMetadata m = res.metadata();
+
+        log.info("Metadata updated for the type: '" + m.typeName() + '\'');
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return MetadataSubCommandsList.UPDATE.text();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataInfoTask.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataInfoTask.java
new file mode 100644
index 0000000..e7a28b5
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataInfoTask.java
@@ -0,0 +1,91 @@
+/*
+ * 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.commandline.meta.tasks;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.internal.commandline.cache.CheckIndexInlineSizes;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataDetailsCommand;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataListCommand;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Task for {@link MetadataListCommand} and {@link MetadataDetailsCommand} commands.
+ */
+@GridInternal
+public class MetadataInfoTask extends VisorMultiNodeTask<MetadataTypeArgs, MetadataListResult, MetadataListResult> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<MetadataTypeArgs, MetadataListResult> job(MetadataTypeArgs arg) {
+        return new MetadataListJob(arg, debug);
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override protected MetadataListResult reduce0(List<ComputeJobResult> results) {
+        if (results.isEmpty())
+            throw new IgniteException("Empty job results");
+
+        if (results.size() > 1)
+            throw new IgniteException("Invalid job results: " + results);
+
+        if (results.get(0).getException() != null)
+            throw results.get(0).getException();
+        else
+            return results.get(0).getData();
+    }
+
+    /**
+     * Job for {@link CheckIndexInlineSizes} command.
+     */
+    private static class MetadataListJob extends VisorJob<MetadataTypeArgs, MetadataListResult> {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Argument.
+         * @param debug Debug.
+         */
+        protected MetadataListJob(@Nullable MetadataTypeArgs arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected MetadataListResult run(@Nullable MetadataTypeArgs arg) throws IgniteException {
+            if (arg == null) {
+                // returns full metadata
+                return new MetadataListResult(
+                    ((CacheObjectBinaryProcessorImpl)ignite.context().cacheObjects()).binaryMetadata());
+            }
+            else {
+                // returns specified metadata
+                int typeId = arg.typeId(ignite.context());
+
+                return new MetadataListResult(Collections.singleton(
+                    ((CacheObjectBinaryProcessorImpl)ignite.context().cacheObjects()).binaryMetadata(typeId)));
+            }
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataListResult.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataListResult.java
new file mode 100644
index 0000000..7bcf6af
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataListResult.java
@@ -0,0 +1,74 @@
+/*
+ * 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.commandline.meta.tasks;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Represents information about cluster metadata.
+ */
+@GridInternal
+public class MetadataListResult extends IgniteDataTransferObject {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Cluster metadata. */
+    private Collection<BinaryMetadata> meta = Collections.emptyList();
+
+    /**
+     * Constructor for optimized marshaller.
+     */
+    public MetadataListResult() {
+        // No-op.
+    }
+
+    /**
+     * @param meta Meta.
+     */
+    public MetadataListResult(Collection<BinaryMetadata> meta) {
+        this.meta = meta;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        U.writeCollection(out, meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(
+        byte protoVer,
+        ObjectInput in
+    ) throws IOException, ClassNotFoundException {
+        meta = U.readCollection(in);
+    }
+
+    /**
+     * @return Cluster binary metadata.
+     */
+    public Collection<BinaryMetadata> metadata() {
+        return meta;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataMarshalled.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataMarshalled.java
new file mode 100644
index 0000000..fbe7c67
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataMarshalled.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta.tasks;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Represents information about cluster metadata.
+ */
+@GridInternal
+public class MetadataMarshalled extends IgniteDataTransferObject {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Marshaled metadata. */
+    private byte[] metaMarshalled;
+
+    /** Metadata. */
+    private BinaryMetadata meta;
+
+    /**
+     * Constructor for optimized marshaller.
+     */
+    public MetadataMarshalled() {
+        // No-op.
+    }
+
+    /**
+     * @param metaMarshalled Marshaled metadata.
+     * @param meta Meta.
+     */
+    public MetadataMarshalled(byte[] metaMarshalled, BinaryMetadata meta) {
+        this.metaMarshalled = metaMarshalled;
+        this.meta = meta;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        U.writeByteArray(out, metaMarshalled);
+        out.writeObject(meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(
+        byte protoVer,
+        ObjectInput in
+    ) throws IOException, ClassNotFoundException {
+        metaMarshalled = U.readByteArray(in);
+        meta = (BinaryMetadata)in.readObject();
+    }
+
+    /**
+     * @return Cluster binary metadata.
+     */
+    public BinaryMetadata metadata() {
+        return meta;
+    }
+
+    /**
+     * @return Marshalled metadata.
+     */
+    public byte[] metadataMarshalled() {
+        return metaMarshalled;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataRemoveTask.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataRemoveTask.java
new file mode 100644
index 0000000..5bd7b7a
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataRemoveTask.java
@@ -0,0 +1,149 @@
+/*
+ * 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.commandline.meta.tasks;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobContext;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.commandline.cache.CheckIndexInlineSizes;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
+import org.apache.ignite.lang.IgniteFuture;
+import org.apache.ignite.lang.IgniteInClosure;
+import org.apache.ignite.lang.IgniteRunnable;
+import org.apache.ignite.plugin.security.SecurityPermission;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.resources.JobContextResource;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Task for {@link MetadataRemoveTask} command.
+ */
+@GridInternal
+public class MetadataRemoveTask extends VisorMultiNodeTask<MetadataTypeArgs, MetadataMarshalled, MetadataMarshalled> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<MetadataTypeArgs, MetadataMarshalled> job(MetadataTypeArgs arg) {
+        return new MetadataRemoveJob(arg, debug);
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override protected MetadataMarshalled reduce0(List<ComputeJobResult> results) {
+        if (results.size() != 1)
+            throw new IgniteException("Invalid job results. Expected exactly 1 result, but was: " + results);
+
+        if (results.get(0).getException() != null)
+            throw results.get(0).getException();
+        else
+            return results.get(0).getData();
+    }
+
+    /**
+     * Job for {@link CheckIndexInlineSizes} command.
+     */
+    private static class MetadataRemoveJob extends VisorJob<MetadataTypeArgs, MetadataMarshalled> {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Auto-inject job context. */
+        @JobContextResource
+        private transient ComputeJobContext jobCtx;
+
+        /** Metadata future. */
+        private transient IgniteFuture<Void> future;
+
+        /** Job result: metadata info for removed type (used for job continuation). */
+        private transient MetadataMarshalled res;
+
+        /**
+         * @param arg Argument.
+         * @param debug Debug.
+         */
+        protected MetadataRemoveJob(@Nullable MetadataTypeArgs arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected MetadataMarshalled run(@Nullable MetadataTypeArgs arg) throws IgniteException {
+            try {
+                if (future == null) {
+                    ignite.context().security().authorize(null, SecurityPermission.ADMIN_METADATA_OPS);
+
+                    assert Objects.nonNull(arg);
+
+                    int typeId = arg.typeId(ignite.context());
+
+                    BinaryMetadata meta = ((CacheObjectBinaryProcessorImpl)ignite.context().cacheObjects())
+                        .binaryMetadata(typeId);
+
+                    byte[] marshalled = U.marshal(ignite.context(), meta);
+
+                    res = new MetadataMarshalled(marshalled, meta);
+
+                    ignite.context().cacheObjects().removeType(typeId);
+
+                    future = ignite.compute().broadcastAsync(new DropAllThinSessionsJob());
+
+                    jobCtx.holdcc();
+
+                    future.listen((IgniteInClosure<IgniteFuture<Void>>)f -> {
+                        if (f.isDone())
+                            jobCtx.callcc();
+                    });
+
+                    return null;
+                }
+
+                return res;
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException(e);
+            }
+        }
+    }
+
+    /**
+     * Job to drop all thin session.
+     */
+    @GridInternal
+    private static class DropAllThinSessionsJob implements IgniteRunnable {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Grid */
+        @IgniteInstanceResource
+        private IgniteEx ignite;
+
+        /** {@inheritDoc} */
+        @Override public void run() throws IgniteException {
+            ignite.context().security().authorize(null, SecurityPermission.ADMIN_METADATA_OPS);
+
+            ignite.context().sqlListener().closeAllSessions();
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataTypeArgs.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataTypeArgs.java
new file mode 100644
index 0000000..b57ca3f
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataTypeArgs.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.commandline.meta.tasks;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/** */
+public class MetadataTypeArgs extends IgniteDataTransferObject {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** Type name argument. */
+    public static final String TYPE_NAME = "--typeName";
+
+    /** Type ID argument. */
+    public static final String TYPE_ID = "--typeId";
+
+    /** Config. */
+    private String typeName;
+
+    /** Metrics. */
+    private Integer typeId;
+
+    /**
+     * Default constructor.
+     */
+    public MetadataTypeArgs() {
+        // No-op.
+    }
+
+    /** */
+    public MetadataTypeArgs(String typeName, Integer typeId) {
+        assert typeName != null ^ typeId != null;
+
+        this.typeName = typeName;
+        this.typeId = typeId;
+    }
+
+    /** */
+    public String typeName() {
+        return typeName;
+    }
+
+    /** */
+    public int typeId(GridKernalContext ctx) {
+        if (typeId != null)
+            return typeId;
+        else
+            return ctx.cacheObjects().typeId(typeName);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        out.writeBoolean(typeName != null);
+
+        if (typeName != null)
+            U.writeString(out, typeName);
+        else
+            out.writeInt(typeId);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(byte protoVer, ObjectInput in) throws IOException, ClassNotFoundException {
+        boolean useName = in.readBoolean();
+
+        if (useName)
+            typeName = U.readString(in);
+        else
+            typeId = in.readInt();
+    }
+
+    /**
+     * @param argIter Command line arguments iterator.
+     * @return Metadata type argument.
+     */
+    public static MetadataTypeArgs parseArguments(CommandArgIterator argIter) {
+        String typeName = null;
+        Integer typeId = null;
+
+        while (argIter.hasNextSubArg() && typeName == null && typeId == null) {
+            String optName = argIter.nextArg("Expecting " + TYPE_NAME + " or " + TYPE_ID);
+
+            switch (optName) {
+                case TYPE_NAME:
+                    typeName = argIter.nextArg("type name");
+
+                    break;
+
+                case TYPE_ID:
+                    typeId = argIter.nextIntArg("typeId");
+
+                    break;
+            }
+        }
+
+        if (typeName == null && typeId == null) {
+            throw new IllegalArgumentException("Type to remove is not specified. " +
+                "Please add one of the options: --typeName <type_name> or --typeId <type_id>");
+        }
+
+        return new MetadataTypeArgs(typeName, typeId);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return typeId != null ? "0x" + Integer.toHexString(typeId).toUpperCase() : typeName;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataUpdateTask.java b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataUpdateTask.java
new file mode 100644
index 0000000..a8dfa68
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/commandline/meta/tasks/MetadataUpdateTask.java
@@ -0,0 +1,99 @@
+/*
+ * 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.commandline.meta.tasks;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.commandline.cache.CheckIndexInlineSizes;
+import org.apache.ignite.internal.commandline.meta.subcommands.MetadataUpdateCommand;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
+import org.apache.ignite.plugin.security.SecurityPermission;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Task for {@link MetadataUpdateCommand} command.
+ */
+@GridInternal
+public class MetadataUpdateTask extends VisorMultiNodeTask<MetadataMarshalled, MetadataMarshalled, MetadataMarshalled> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<MetadataMarshalled, MetadataMarshalled> job(MetadataMarshalled arg) {
+        return new MetadataUpdateJob(arg, debug);
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override protected MetadataMarshalled reduce0(List<ComputeJobResult> results) {
+        if (results.size() != 1)
+            throw new IgniteException("Invalid job results. Expected exactly 1 result, but was: " + results);
+
+        if (results.get(0).getException() != null)
+            throw results.get(0).getException();
+        else
+            return results.get(0).getData();
+    }
+
+    /**
+     * Job for {@link CheckIndexInlineSizes} command.
+     */
+    private static class MetadataUpdateJob extends VisorJob<MetadataMarshalled, MetadataMarshalled> {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Argument.
+         * @param debug Debug.
+         */
+        protected MetadataUpdateJob(@Nullable MetadataMarshalled arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected MetadataMarshalled run(@Nullable MetadataMarshalled arg) throws IgniteException {
+            ignite.context().security().authorize(null, SecurityPermission.ADMIN_METADATA_OPS);
+
+            assert Objects.nonNull(arg);
+
+            byte[] marshalled = arg.metadataMarshalled();
+
+            try {
+                BinaryMetadata meta = U.unmarshal(
+                    ignite.context(),
+                    marshalled,
+                    U.resolveClassLoader(ignite.context().config()));
+
+                ((CacheObjectBinaryProcessorImpl)ignite.context().cacheObjects()).binaryContext()
+                    .updateMetadata(meta.typeId(), meta, false);
+
+                return new MetadataMarshalled(null, meta);
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException(e);
+            }
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
index c35ee28..bb530fd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
@@ -250,10 +250,10 @@ public class JdbcThinConnection implements Connection {
     private final IgniteProductVersion baseEndpointVer;
 
     /** Binary context. */
-    private final BinaryContext ctx;
+    private volatile BinaryContext ctx;
 
     /** Binary metadata handler. */
-    private final JdbcBinaryMetadataHandler metaHnd;
+    private volatile JdbcBinaryMetadataHandler metaHnd;
 
     /** Marshaller context. */
     private final JdbcMarshallerContext marshCtx;
@@ -1333,6 +1333,10 @@ public class JdbcThinConnection implements Connection {
 
             stmts.clear();
         }
+
+        // Clear local metadata cache on disconnect.
+        metaHnd = new JdbcBinaryMetadataHandler();
+        ctx = createBinaryCtx(metaHnd, marshCtx);
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataFileStore.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataFileStore.java
index 1614a0a..1c9540c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataFileStore.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataFileStore.java
@@ -167,6 +167,32 @@ class BinaryMetadataFileStore {
     }
 
     /**
+     * Remove metadata for specified type.
+     *
+     * @param typeId Type identifier.
+     */
+    private void removeMeta(int typeId) {
+        if (!isPersistenceEnabled)
+            return;
+
+        File file = new File(metadataDir, typeId + ".bin");
+
+        if (!file.delete()) {
+            final String msg = "Failed to remove metadata for typeId: " + typeId;
+
+            U.error(log, msg);
+
+            writer.cancel();
+
+            IgniteException e = new IgniteException(msg);
+
+            ctx.failure().process(new FailureContext(FailureType.CRITICAL_ERROR, e));
+
+            throw e;
+        }
+    }
+
+    /**
      * Restores metadata on startup of {@link CacheObjectBinaryProcessorImpl} but before starting discovery.
      */
     void restoreMetadata() {
@@ -244,7 +270,17 @@ class BinaryMetadataFileStore {
         if (!isPersistenceEnabled)
             return;
 
-        writer.startWritingAsync(typeId, typeVer);
+        writer.startTaskAsync(typeId, typeVer);
+    }
+
+    /**
+     * @param typeId Type ID.
+     */
+    public void removeMetadataAsync(int typeId) {
+        if (!isPersistenceEnabled)
+            return;
+
+        writer.startTaskAsync(typeId, BinaryMetadataTransport.REMOVED_VERSION);
     }
 
     /**
@@ -272,7 +308,7 @@ class BinaryMetadataFileStore {
         if (!isPersistenceEnabled)
             return;
 
-        writer.finishWriteFuture(typeId, typeVer);
+        writer.finishWriteFuture(typeId, typeVer, null);
     }
 
     /**
@@ -315,33 +351,46 @@ class BinaryMetadataFileStore {
     }
 
     /**
+     * @param typeId Type ID.
+     */
+    void prepareMetadataRemove(int typeId) {
+        if (!isPersistenceEnabled)
+            return;
+
+        writer.cancelTasksForType(typeId);
+
+        writer.prepareRemoveFuture(typeId);
+    }
+
+    /**
      *
      */
     private class BinaryMetadataAsyncWriter extends GridWorker {
         /**
          * Queue of write tasks submitted for execution.
          */
-        private final BlockingQueue<WriteOperationTask> queue = new LinkedBlockingQueue<>();
+        private final BlockingQueue<OperationTask> queue = new LinkedBlockingQueue<>();
 
         /**
          * Write operation tasks prepared for writing (but not yet submitted to execution (actual writing).
          */
-        private final ConcurrentMap<OperationSyncKey, WriteOperationTask> preparedWriteTasks = new ConcurrentHashMap<>();
+        private final ConcurrentMap<OperationSyncKey, OperationTask> preparedTasks = new ConcurrentHashMap<>();
 
         /** */
         BinaryMetadataAsyncWriter() {
-            super(ctx.igniteInstanceName(), "binary-metadata-writer", BinaryMetadataFileStore.this.log, ctx.workersRegistry());
+            super(ctx.igniteInstanceName(), "binary-metadata-writer",
+                BinaryMetadataFileStore.this.log, ctx.workersRegistry());
         }
 
         /**
          * @param typeId Type ID.
          * @param typeVer Type version.
          */
-        synchronized void startWritingAsync(int typeId, int typeVer) {
+        synchronized void startTaskAsync(int typeId, int typeVer) {
             if (isCancelled())
                 return;
 
-            WriteOperationTask task = preparedWriteTasks.get(new OperationSyncKey(typeId, typeVer));
+            OperationTask task = preparedTasks.get(new OperationSyncKey(typeId, typeVer));
 
             if (task != null) {
                 if (log.isDebugEnabled())
@@ -371,7 +420,7 @@ class BinaryMetadataFileStore {
 
             IgniteCheckedException err = new IgniteCheckedException("Operation has been cancelled (node is stopping).");
 
-            for (Map.Entry<OperationSyncKey, WriteOperationTask> e : preparedWriteTasks.entrySet()) {
+            for (Map.Entry<OperationSyncKey, OperationTask> e : preparedTasks.entrySet()) {
                 if (log.isDebugEnabled())
                     log.debug(
                         "Cancelling future for write operation for" +
@@ -382,7 +431,7 @@ class BinaryMetadataFileStore {
                 e.getValue().future.onDone(err);
             }
 
-            preparedWriteTasks.clear();
+            preparedTasks.clear();
         }
 
         /** {@inheritDoc} */
@@ -403,7 +452,7 @@ class BinaryMetadataFileStore {
 
         /** */
         private void body0() throws InterruptedException {
-            WriteOperationTask task;
+            OperationTask task;
 
             blockingSectionBegin();
 
@@ -413,27 +462,52 @@ class BinaryMetadataFileStore {
                 if (log.isDebugEnabled())
                     log.debug(
                         "Starting write operation for" +
-                            " [typeId=" + task.meta.typeId() +
-                            ", typeVer=" + task.typeVer + ']'
+                            " [typeId=" + task.typeId() +
+                            ", typeVer=" + task.typeVersion() + ']'
                     );
 
-                writeMetadata(task.meta);
+                task.execute(BinaryMetadataFileStore.this);
             }
             finally {
                 blockingSectionEnd();
             }
 
-            finishWriteFuture(task.meta.typeId(), task.typeVer);
+            finishWriteFuture(task.typeId(), task.typeVersion(), task);
+        }
+
+        /**
+         * @param typeId Binary metadata type id.
+         */
+        synchronized void cancelTasksForType(int typeId) {
+            final IgniteCheckedException err = new IgniteCheckedException("Operation has been cancelled by type remove.");
+
+            preparedTasks.entrySet().removeIf(entry -> {
+                if (entry.getKey().typeId == typeId) {
+                    entry.getValue().future().onDone(err);
+
+                    return true;
+                }
+                return false;
+            });
         }
 
         /**
          * @param typeId Binary metadata type id.
          * @param typeVer Type version.
+         * @param task Task to remove.
          */
-        void finishWriteFuture(int typeId, int typeVer) {
-            WriteOperationTask task = preparedWriteTasks.remove(new OperationSyncKey(typeId, typeVer));
+        void finishWriteFuture(int typeId, int typeVer, OperationTask task) {
+            boolean removed;
 
-            if (task != null) {
+            if (task != null)
+                removed = preparedTasks.remove(new OperationSyncKey(typeId, typeVer), task);
+            else {
+                task = preparedTasks.remove(new OperationSyncKey(typeId, typeVer));
+
+                removed = task != null;
+            }
+
+            if (removed) {
                 if (log.isDebugEnabled())
                     log.debug(
                         "Future for write operation for" +
@@ -471,7 +545,23 @@ class BinaryMetadataFileStore {
                         ", typeVersion=" + typeVer + ']'
                 );
 
-            preparedWriteTasks.putIfAbsent(new OperationSyncKey(meta.typeId(), typeVer), new WriteOperationTask(meta, typeVer));
+            preparedTasks.putIfAbsent(new OperationSyncKey(meta.typeId(), typeVer), new WriteOperationTask(meta, typeVer));
+        }
+
+        /**
+         */
+        synchronized void prepareRemoveFuture(int typeId) {
+            if (isCancelled())
+                return;
+
+            if (log.isDebugEnabled())
+                log.debug(
+                    "Prepare task for async remove for" +
+                        "[typeId=" + typeId + ']'
+                );
+
+            preparedTasks.putIfAbsent(new OperationSyncKey(typeId, BinaryMetadataTransport.REMOVED_VERSION),
+                new RemoveOperationTask(typeId));
         }
 
         /**
@@ -488,7 +578,7 @@ class BinaryMetadataFileStore {
                 return;
             }
 
-            WriteOperationTask task = preparedWriteTasks.get(new OperationSyncKey(typeId, typeVer));
+            OperationTask task = preparedTasks.get(new OperationSyncKey(typeId, typeVer));
 
             if (task != null) {
                 if (log.isDebugEnabled())
@@ -524,21 +614,89 @@ class BinaryMetadataFileStore {
     /**
      *
      */
-    private static final class WriteOperationTask {
+    private abstract static class OperationTask {
         /** */
-        private final BinaryMetadata meta;
+        private final GridFutureAdapter<Void> future = new GridFutureAdapter<>();
 
         /** */
-        private final int typeVer;
+        abstract void execute(BinaryMetadataFileStore store);
 
         /** */
-        private final GridFutureAdapter future = new GridFutureAdapter();
+        abstract int typeId();
 
         /** */
+        abstract int typeVersion();
+
+        /**
+         * @return Task future.
+         */
+        GridFutureAdapter<Void> future() {
+            return future;
+        }
+    }
+
+    /**
+     *
+     */
+    private static final class WriteOperationTask extends OperationTask {
+        /** */
+        private final BinaryMetadata meta;
+
+        /** */
+        private final int typeVer;
+
+        /**
+         * @param meta Metadata for binary type.
+         * @param ver Version of type.
+         */
         private WriteOperationTask(BinaryMetadata meta, int ver) {
             this.meta = meta;
             typeVer = ver;
         }
+
+        /** {@inheritDoc} */
+        @Override void execute(BinaryMetadataFileStore store) {
+            store.writeMetadata(meta);
+        }
+
+        /** {@inheritDoc} */
+        @Override int typeId() {
+            return meta.typeId();
+        }
+
+        /** {@inheritDoc} */
+        @Override int typeVersion() {
+            return typeVer;
+        }
+    }
+
+    /**
+     *
+     */
+    private static final class RemoveOperationTask extends OperationTask {
+        /** */
+        private final int typeId;
+
+        /**
+         */
+        private RemoveOperationTask(int typeId) {
+            this.typeId = typeId;
+        }
+
+        /** {@inheritDoc} */
+        @Override void execute(BinaryMetadataFileStore store) {
+            store.removeMeta(typeId);
+        }
+
+        /** {@inheritDoc} */
+        @Override int typeId() {
+            return typeId;
+        }
+
+        /** {@inheritDoc} */
+        @Override int typeVersion() {
+            return BinaryMetadataTransport.REMOVED_VERSION;
+        }
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataHolder.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataHolder.java
index 4e783ca..923434d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataHolder.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataHolder.java
@@ -36,17 +36,38 @@ final class BinaryMetadataHolder implements Serializable {
     /** */
     private final int acceptedVer;
 
+    /** */
+    private final transient boolean removing;
+
     /**
      * @param metadata Metadata.
      * @param pendingVer Version of this metadata - how many updates were issued for this type.
      * @param acceptedVer Pending updates count.
      */
     BinaryMetadataHolder(BinaryMetadata metadata, int pendingVer, int acceptedVer) {
+        this(metadata, pendingVer, acceptedVer, false);
+    }
+
+    /**
+     * @param metadata Metadata.
+     * @param pendingVer Pending updates count.
+     * @param acceptedVer Version of this metadata - how many updates were issued for this type.
+     * @param removing Flag means the metadata is removing now.
+     */
+    private BinaryMetadataHolder(BinaryMetadata metadata, int pendingVer, int acceptedVer, boolean removing) {
         assert metadata != null;
 
         this.metadata = metadata;
         this.pendingVer = pendingVer;
         this.acceptedVer = acceptedVer;
+        this.removing = removing;
+    }
+
+    /**
+     * @return Holder metadata with remove state where remove pending message has been handled.
+     */
+    BinaryMetadataHolder createRemoving() {
+        return new BinaryMetadataHolder(metadata, pendingVer, acceptedVer, true);
     }
 
     /**
@@ -70,10 +91,19 @@ final class BinaryMetadataHolder implements Serializable {
         return acceptedVer;
     }
 
+    /**
+     * @return {@code true} is the metadata is removing now.
+     */
+    boolean removing() {
+        return removing;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return "[typeId=" + metadata.typeId() +
             ", pendingVer=" + pendingVer +
-            ", acceptedVer=" + acceptedVer + "]";
+            ", acceptedVer=" + acceptedVer +
+            ", removing=" + removing +
+            "]";
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataTransport.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataTransport.java
index 7b89632..aa52237 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataTransport.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataTransport.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.cluster.ClusterNode;
@@ -37,6 +38,7 @@ import org.apache.ignite.events.Event;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.GridTopic;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.binary.BinaryContext;
 import org.apache.ignite.internal.binary.BinaryMetadata;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.managers.communication.GridIoManager;
@@ -46,6 +48,7 @@ import org.apache.ignite.internal.managers.discovery.GridDiscoveryManager;
 import org.apache.ignite.internal.managers.eventstorage.GridLocalEventListener;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
+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.lang.IgniteInClosure;
@@ -65,6 +68,9 @@ import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYS
  * around protocols.
  */
 final class BinaryMetadataTransport {
+    /** Special metadata version for remove. */
+    public static final int REMOVED_VERSION = -2;
+
     /** */
     private final GridDiscoveryManager discoMgr;
 
@@ -90,7 +96,7 @@ final class BinaryMetadataTransport {
     private final ConcurrentMap<SyncKey, MetadataUpdateResultFuture> syncMap = new ConcurrentHashMap<>();
 
     /** It store pending update future for typeId. It allow to do only one update in one moment. */
-    private final ConcurrentMap<Integer, MetadataUpdateResultFuture> pendingTypeIdMap = new ConcurrentHashMap<>();
+    private final ConcurrentMap<Integer, GridFutureAdapter<?>> pendingTypeIdMap = new ConcurrentHashMap<>();
 
     /** */
     private final ConcurrentMap<Integer, ClientMetadataRequestFuture> clientReqSyncMap = new ConcurrentHashMap<>();
@@ -99,10 +105,16 @@ final class BinaryMetadataTransport {
     private final ConcurrentMap<SyncKey, GridFutureAdapter<?>> schemaWaitFuts = new ConcurrentHashMap<>();
 
     /** */
-    private volatile boolean stopping;
+    private final List<BinaryMetadataUpdatedListener> binaryUpdatedLsnrs = new CopyOnWriteArrayList<>();
 
     /** */
-    private final List<BinaryMetadataUpdatedListener> binaryUpdatedLsnrs = new CopyOnWriteArrayList<>();
+    private final BinaryContext binCtx;
+
+    /** */
+    private final boolean isPersistenceEnabled;
+
+    /** */
+    private volatile boolean stopping;
 
     /**
      * @param metaLocCache Metadata locale cache.
@@ -113,24 +125,25 @@ final class BinaryMetadataTransport {
     BinaryMetadataTransport(
         ConcurrentMap<Integer, BinaryMetadataHolder> metaLocCache,
         BinaryMetadataFileStore metadataFileStore,
+        BinaryContext binCtx,
         final GridKernalContext ctx,
         IgniteLogger log
     ) {
         this.metaLocCache = metaLocCache;
-
         this.metadataFileStore = metadataFileStore;
-
         this.ctx = ctx;
-
+        this.binCtx = binCtx;
         this.log = log;
 
         discoMgr = ctx.discovery();
 
         clientNode = ctx.clientNode();
+        isPersistenceEnabled = CU.isPersistenceEnabled(ctx.config()) && !clientNode;
 
         discoMgr.setCustomEventListener(MetadataUpdateProposedMessage.class, new MetadataUpdateProposedListener());
-
         discoMgr.setCustomEventListener(MetadataUpdateAcceptedMessage.class, new MetadataUpdateAcceptedListener());
+        discoMgr.setCustomEventListener(MetadataRemoveProposedMessage.class, new MetadataRemoveProposedListener());
+        discoMgr.setCustomEventListener(MetadataRemoveAcceptedMessage.class, new MetadataRemoveAcceptedListener());
 
         GridIoManager ioMgr = ctx.io();
 
@@ -153,6 +166,19 @@ final class BinaryMetadataTransport {
     }
 
     /**
+     * Method checks if arrived metadata is obsolete comparing to the one from local cache.
+     *
+     * @param locP pendingVersion of metadata from local cache.
+     * @param locA acceptedVersion of metadata from local cache.
+     * @param remP pendingVersion of metadata from arrived message (client response/proposed/accepted).
+     * @param remA acceptedVersion of metadata from arrived message (client response/proposed/accepted).
+     * @return {@code true} is
+     */
+    private static boolean obsoleteUpdate(int locP, int locA, int remP, int remA) {
+        return (remP < locP) || (remP == locP && remA < locA);
+    }
+
+    /**
      * Adds BinaryMetadata updates {@link BinaryMetadataUpdatedListener listener} to transport.
      *
      * @param lsnr Listener.
@@ -175,6 +201,9 @@ final class BinaryMetadataTransport {
         do {
             BinaryMetadataHolder metaHolder = metaLocCache.get(typeId);
 
+            if (metaHolder != null && metaHolder.removing())
+                throw new IgniteException("The metadata is removing for type [typeId=" + typeId + ']');
+
             BinaryMetadata oldMeta = Optional.ofNullable(metaHolder)
                 .map(BinaryMetadataHolder::metadata)
                 .orElse(null);
@@ -242,11 +271,11 @@ final class BinaryMetadataTransport {
      * Put new update future and it are waiting pending future if it exists.
      *
      * @param typeId Type id.
-     * @param metaUpdateFut New metadata update future.
+     * @param newMetaFut New metadata update / remove future.
      * @return {@code true} If given future put successfully.
      */
-    private boolean putAndWaitPendingUpdate(int typeId, MetadataUpdateResultFuture metaUpdateFut) {
-        MetadataUpdateResultFuture oldFut = pendingTypeIdMap.putIfAbsent(typeId, metaUpdateFut);
+    private boolean putAndWaitPendingUpdate(int typeId, GridFutureAdapter<?> newMetaFut) {
+        GridFutureAdapter<?> oldFut = pendingTypeIdMap.putIfAbsent(typeId, newMetaFut);
 
         if (oldFut != null) {
             try {
@@ -288,6 +317,27 @@ final class BinaryMetadataTransport {
     }
 
     /**
+     * Allows thread to wait for a metadata of given typeId and version to be removed.
+     *
+     * @param typeId ID of binary type.
+     * @return future to wait for update result on.
+     */
+    GridFutureAdapter<MetadataUpdateResult> awaitMetadataRemove(int typeId) {
+        SyncKey key = new SyncKey(typeId, REMOVED_VERSION);
+        MetadataUpdateResultFuture resFut = new MetadataUpdateResultFuture(key);
+
+        MetadataUpdateResultFuture oldFut = syncMap.putIfAbsent(key, resFut);
+
+        if (oldFut != null)
+            resFut = oldFut;
+
+        if (!metaLocCache.containsKey(typeId))
+            resFut.onDone(MetadataUpdateResult.createSuccessfulResult(-1));
+
+        return resFut;
+    }
+
+    /**
      * Await specific schema update.
      *
      * @param typeId Type id.
@@ -351,14 +401,137 @@ final class BinaryMetadataTransport {
             fut.onDone(res);
     }
 
-    /** */
-    private final class MetadataUpdateProposedListener implements CustomEventListener<MetadataUpdateProposedMessage> {
+    /**
+     * Remove metadata from cluster for specified type.
+     *
+     * @param typeId Type ID to remove metadata.
+     */
+    public GridFutureAdapter<MetadataUpdateResult> requestMetadataRemove(int typeId) {
+        MetadataUpdateResultFuture resFut;
+
+        do {
+            resFut = new MetadataUpdateResultFuture(typeId);
+        }
+        while (!putAndWaitPendingUpdate(typeId, resFut));
+
+        try {
+            synchronized (this) {
+                unlabeledFutures.add(resFut);
+
+                if (!stopping)
+                    discoMgr.sendCustomEvent(new MetadataRemoveProposedMessage(typeId, ctx.localNodeId()));
+                else
+                    resFut.onDone(MetadataUpdateResult.createUpdateDisabledResult());
+            }
+        }
+        catch (Exception e) {
+            resFut.onDone(MetadataUpdateResult.createUpdateDisabledResult(), e);
+        }
+
+        if (ctx.clientDisconnected())
+            onDisconnected();
+
+        return resFut;
+    }
+
+    /**
+     * @param typeId Type ID.
+     * @param pendingVer Pending version.
+     * @param fut Future.
+     */
+    private void initSyncFor(int typeId, int pendingVer, final MetadataUpdateResultFuture fut) {
+        if (stopping) {
+            fut.onDone(MetadataUpdateResult.createUpdateDisabledResult());
+
+            return;
+        }
+
+        SyncKey key = new SyncKey(typeId, pendingVer);
+
+        MetadataUpdateResultFuture oldFut = syncMap.putIfAbsent(key, fut);
+
+        if (oldFut != null) {
+            oldFut.listen(new IgniteInClosure<IgniteInternalFuture<MetadataUpdateResult>>() {
+                @Override public void apply(IgniteInternalFuture<MetadataUpdateResult> doneFut) {
+                    fut.onDone(doneFut.result(), doneFut.error());
+                }
+            });
+        }
+
+        fut.key(key);
+    }
+
+    /**
+     * Key for mapping arriving {@link MetadataUpdateAcceptedMessage} messages to {@link MetadataUpdateResultFuture}s
+     * other threads may be waiting on.
+     */
+    private static final class SyncKey {
+        /**
+         *
+         */
+        private final int typeId;
+
+        /**
+         *
+         */
+        private final int ver;
+
+        /**
+         * @param typeId Type id.
+         * @param ver Version.
+         */
+        private SyncKey(int typeId, int ver) {
+            this.typeId = typeId;
+            this.ver = ver;
+        }
 
+        /**
+         * @return Type ID.
+         */
+        int typeId() {
+            return typeId;
+        }
+
+        /**
+         * @return Version.
+         */
+        int version() {
+            return ver;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return typeId + ver;
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (o == this)
+                return true;
+
+            if (!(o instanceof SyncKey))
+                return false;
+
+            SyncKey that = (SyncKey)o;
+
+            return (typeId == that.typeId) && (ver == that.ver);
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(SyncKey.class, this);
+        }
+    }
+
+    /**
+     *
+     */
+    private final class MetadataUpdateProposedListener implements CustomEventListener<MetadataUpdateProposedMessage> {
         /** {@inheritDoc} */
         @Override public void onCustomEvent(AffinityTopologyVersion topVer, ClusterNode snd,
             MetadataUpdateProposedMessage msg) {
             if (log.isDebugEnabled())
-                log.debug("Received MetadataUpdateProposedListener [typeId=" + msg.typeId() +
+                log.debug("Received MetadataUpdateProposed message [typeId=" + msg.typeId() +
                     ", typeName=" + msg.metadata().typeName() +
                     ", pendingVer=" + msg.pendingVersion() +
                     ", acceptedVer=" + msg.acceptedVersion() +
@@ -374,38 +547,48 @@ final class BinaryMetadataTransport {
             if (msg.pendingVersion() == 0) {
                 //coordinator receives update request
                 if (holder != null) {
-                    pendingVer = holder.pendingVersion() + 1;
-                    acceptedVer = holder.acceptedVersion();
+                    if (holder.removing()) {
+                        msg.markRejected(new BinaryObjectException("The type is removing now [typeId=" + typeId + ']'));
+
+                        pendingVer = REMOVED_VERSION;
+                        acceptedVer = REMOVED_VERSION;
+                    }
+                    else {
+                        pendingVer = holder.pendingVersion() + 1;
+                        acceptedVer = holder.acceptedVersion();
+                    }
                 }
                 else {
                     pendingVer = 1;
                     acceptedVer = 0;
                 }
 
-                msg.pendingVersion(pendingVer);
-                msg.acceptedVersion(acceptedVer);
+                if (!msg.rejected()) {
+                    msg.pendingVersion(pendingVer);
+                    msg.acceptedVersion(acceptedVer);
 
-                BinaryMetadata locMeta = holder != null ? holder.metadata() : null;
+                    BinaryMetadata locMeta = holder != null ? holder.metadata() : null;
 
-                try {
-                    Set<Integer> changedSchemas = new LinkedHashSet<>();
+                    try {
+                        Set<Integer> changedSchemas = new LinkedHashSet<>();
 
-                    BinaryMetadata mergedMeta = mergeMetadata(locMeta, msg.metadata(), changedSchemas);
+                        BinaryMetadata mergedMeta = mergeMetadata(locMeta, msg.metadata(), changedSchemas);
 
-                    if (log.isDebugEnabled())
-                        log.debug("Versions are stamped on coordinator" +
-                            " [typeId=" + typeId +
-                            ", changedSchemas=" + changedSchemas +
-                            ", pendingVer=" + pendingVer +
-                            ", acceptedVer=" + acceptedVer + "]"
-                        );
-
-                    msg.metadata(mergedMeta);
-                }
-                catch (BinaryObjectException err) {
-                    log.warning("Exception with merging metadata for typeId: " + typeId, err);
+                        if (log.isDebugEnabled())
+                            log.debug("Versions are stamped on coordinator" +
+                                " [typeId=" + typeId +
+                                ", changedSchemas=" + changedSchemas +
+                                ", pendingVer=" + pendingVer +
+                                ", acceptedVer=" + acceptedVer + "]"
+                            );
+
+                        msg.metadata(mergedMeta);
+                    }
+                    catch (BinaryObjectException err) {
+                        log.warning("Exception with merging metadata for typeId: " + typeId, err);
 
-                    msg.markRejected(err);
+                        msg.markRejected(err);
+                    }
                 }
             }
             else {
@@ -466,7 +649,7 @@ final class BinaryMetadataTransport {
             }
             else {
                 if (!msg.rejected()) {
-                    BinaryMetadata locMeta = holder != null ? holder.metadata() : null;
+                    BinaryMetadata locMeta = holder != null && !holder.removing() ? holder.metadata() : null;
 
                     Set<Integer> changedSchemas = new LinkedHashSet<>();
 
@@ -512,37 +695,9 @@ final class BinaryMetadataTransport {
     }
 
     /**
-     * @param typeId Type ID.
-     * @param pendingVer Pending version.
-     * @param fut Future.
-     */
-    private void initSyncFor(int typeId, int pendingVer, final MetadataUpdateResultFuture fut) {
-        if (stopping) {
-            fut.onDone(MetadataUpdateResult.createUpdateDisabledResult());
-
-            return;
-        }
-
-        SyncKey key = new SyncKey(typeId, pendingVer);
-
-        MetadataUpdateResultFuture oldFut = syncMap.putIfAbsent(key, fut);
-
-        if (oldFut != null) {
-            oldFut.listen(new IgniteInClosure<IgniteInternalFuture<MetadataUpdateResult>>() {
-                @Override public void apply(IgniteInternalFuture<MetadataUpdateResult> doneFut) {
-                    fut.onDone(doneFut.result(), doneFut.error());
-                }
-            });
-        }
-
-        fut.key(key);
-    }
-
-    /**
      *
      */
     private final class MetadataUpdateAcceptedListener implements CustomEventListener<MetadataUpdateAcceptedMessage> {
-
         /** {@inheritDoc} */
         @Override public void onCustomEvent(AffinityTopologyVersion topVer, ClusterNode snd,
             MetadataUpdateAcceptedMessage msg) {
@@ -592,7 +747,8 @@ final class BinaryMetadataTransport {
 
                 metadataFileStore.writeMetadataAsync(typeId, newAcceptedVer);
 
-                metaLocCache.put(typeId, new BinaryMetadataHolder(holder.metadata(), holder.pendingVersion(), newAcceptedVer));
+                metaLocCache.put(typeId,
+                    new BinaryMetadataHolder(holder.metadata(), holder.pendingVersion(), newAcceptedVer));
             }
 
             for (BinaryMetadataUpdatedListener lsnr : binaryUpdatedLsnrs)
@@ -632,6 +788,9 @@ final class BinaryMetadataTransport {
      */
     public final class MetadataUpdateResultFuture extends GridFutureAdapter<MetadataUpdateResult> {
         /** */
+        private SyncKey key;
+
+        /** */
         MetadataUpdateResultFuture(int typeId) {
             this.key = new SyncKey(typeId, 0);
         }
@@ -643,9 +802,6 @@ final class BinaryMetadataTransport {
             this.key = key;
         }
 
-        /** */
-        private SyncKey key;
-
         /** {@inheritDoc} */
         @Override public boolean onDone(@Nullable MetadataUpdateResult res, @Nullable Throwable err) {
             assert res != null;
@@ -679,64 +835,6 @@ final class BinaryMetadataTransport {
     }
 
     /**
-     * Key for mapping arriving {@link MetadataUpdateAcceptedMessage} messages to {@link MetadataUpdateResultFuture}s
-     * other threads may be waiting on.
-     */
-    private static final class SyncKey {
-        /** */
-        private final int typeId;
-
-        /** */
-        private final int ver;
-
-        /**
-         * @param typeId Type id.
-         * @param ver Version.
-         */
-        private SyncKey(int typeId, int ver) {
-            this.typeId = typeId;
-            this.ver = ver;
-        }
-
-        /**
-         * @return Type ID.
-         */
-        int typeId() {
-            return typeId;
-        }
-
-        /**
-         * @return Version.
-         */
-        int version() {
-            return ver;
-        }
-
-        /** {@inheritDoc} */
-        @Override public int hashCode() {
-            return typeId + ver;
-        }
-
-        /** {@inheritDoc} */
-        @Override public boolean equals(Object o) {
-            if (o == this)
-                return true;
-
-            if (!(o instanceof SyncKey))
-                return false;
-
-            SyncKey that = (SyncKey)o;
-
-            return (typeId == that.typeId) && (ver == that.ver);
-        }
-
-        /** {@inheritDoc} */
-        @Override public String toString() {
-            return S.toString(SyncKey.class, this);
-        }
-    }
-
-    /**
      * Listener is registered on each server node in cluster waiting for metadata requests from clients.
      */
     private final class MetadataRequestListener implements GridMessageListener {
@@ -844,15 +942,97 @@ final class BinaryMetadataTransport {
     }
 
     /**
-     * Method checks if arrived metadata is obsolete comparing to the one from local cache.
      *
-     * @param locP pendingVersion of metadata from local cache.
-     * @param locA acceptedVersion of metadata from local cache.
-     * @param remP pendingVersion of metadata from arrived message (client response/proposed/accepted).
-     * @param remA acceptedVersion of metadata from arrived message (client response/proposed/accepted).
-     * @return {@code true} is
      */
-    private static boolean obsoleteUpdate(int locP, int locA, int remP, int remA) {
-        return (remP < locP) || (remP == locP && remA < locA);
+    private final class MetadataRemoveProposedListener implements CustomEventListener<MetadataRemoveProposedMessage> {
+        /** {@inheritDoc} */
+        @Override public void onCustomEvent(AffinityTopologyVersion topVer, ClusterNode snd,
+            MetadataRemoveProposedMessage msg) {
+            if (log.isDebugEnabled())
+                log.debug("Received MetadataRemoveProposed message: " + msg);
+
+            int typeId = msg.typeId();
+
+            BinaryMetadataHolder metaHld = metaLocCache.get(typeId);
+
+            assert metaHld != null : "No metadata found for typeId: " + typeId;
+
+            if (msg.isOnCoordinator()) {
+                if (metaHld == null)
+                    msg.markRejected(new BinaryObjectException("Type not found [typeId=" + typeId + ']'));
+
+                if (metaHld.pendingVersion() != metaHld.acceptedVersion()) {
+                    msg.markRejected(new BinaryObjectException(
+                        "Remove type failed. " +
+                            "Type is being updated now [typeId=" + typeId
+                            + ", pendingVersion=" + metaHld.pendingVersion()
+                            + ", acceptedVersion=" + metaHld.acceptedVersion()
+                            + ']'));
+                }
+
+                if (metaHld.removing()) {
+                    msg.markRejected(new BinaryObjectException(
+                        "Remove type failed. " +
+                            "Type is being removed now [typeId=" + typeId
+                            + ']'));
+                }
+
+                msg.setOnCoordinator(false);
+            }
+
+            MetadataUpdateResultFuture fut = null;
+
+            if (msg.origNodeId().equals(ctx.localNodeId()))
+                fut = unlabeledFutures.poll();
+
+            if (msg.rejected()) {
+                if (fut != null)
+                    fut.onDone(MetadataUpdateResult.createFailureResult(msg.rejectionError()));
+            }
+            else {
+                if (fut != null)
+                    initSyncFor(typeId, REMOVED_VERSION, fut);
+
+                metaLocCache.put(typeId, metaHld.createRemoving());
+
+                if (isPersistenceEnabled)
+                    metadataFileStore.prepareMetadataRemove(typeId);
+            }
+        }
+    }
+
+    /**
+     *
+     */
+    private final class MetadataRemoveAcceptedListener implements CustomEventListener<MetadataRemoveAcceptedMessage> {
+        /** {@inheritDoc} */
+        @Override public void onCustomEvent(AffinityTopologyVersion topVer, ClusterNode snd,
+            MetadataRemoveAcceptedMessage msg) {
+            if (log.isDebugEnabled())
+                log.debug("Received MetadataRemoveAccepted message: " + msg);
+
+            if (msg.duplicated())
+                return;
+
+            final int typeId = msg.typeId();
+
+            if (!metaLocCache.containsKey(typeId)) {
+                msg.duplicated(true);
+
+                return;
+            }
+
+            if (isPersistenceEnabled)
+                metadataFileStore.removeMetadataAsync(typeId);
+
+            GridFutureAdapter<MetadataUpdateResult> fut = syncMap.get(new SyncKey(typeId, REMOVED_VERSION));
+
+            metaLocCache.remove(typeId);
+
+            binCtx.removeType(typeId);
+
+            if (fut != null)
+                fut.onDone(MetadataUpdateResult.createSuccessfulResult(REMOVED_VERSION));
+        }
     }
 }
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 61e05e3..cdc9e41 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
@@ -52,6 +52,7 @@ import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteFeatures;
 import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.IgniteNodeAttributes;
@@ -246,8 +247,6 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
                 metadataFileStore.start();
             }
 
-            transport = new BinaryMetadataTransport(metadataLocCache, metadataFileStore, ctx, log);
-
             BinaryMetadataHandler metaHnd = new BinaryMetadataHandler() {
                 @Override public void addMeta(
                     int typeId,
@@ -306,6 +305,8 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
                 new TestBinaryContext(metaHnd, ctx.config(), ctx.log(BinaryContext.class)) :
                 new BinaryContext(metaHnd, ctx.config(), ctx.log(BinaryContext.class));
 
+            transport = new BinaryMetadataTransport(metadataLocCache, metadataFileStore, binaryCtx, ctx, log);
+
             IgniteUtils.invoke(BinaryMarshaller.class, bMarsh0, "setBinaryContext", binaryCtx, ctx.config());
 
             binaryMarsh = new GridBinaryMarshaller(binaryCtx);
@@ -740,6 +741,19 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
         }
 
         if (holder != null) {
+            if (holder.removing()) {
+                GridFutureAdapter<MetadataUpdateResult> fut = transport.awaitMetadataRemove(typeId);
+
+                try {
+                    fut.get();
+                }
+                catch (IgniteCheckedException ignored) {
+                    // No-op.
+                }
+
+                return null;
+            }
+
             if (curThread instanceof IgniteDiscoveryThread || (curThread != null && curThread.isForbiddenToRequestBinaryMetadata()))
                 return holder.metadata();
 
@@ -924,6 +938,28 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
         });
     }
 
+    /**
+     * @return Cluster binary metadata.
+     * @throws BinaryObjectException on error.
+     */
+    public Collection<BinaryMetadata> binaryMetadata() throws BinaryObjectException {
+        return F.viewReadOnly(metadataLocCache.values(), new IgniteClosure<BinaryMetadataHolder, BinaryMetadata>() {
+            @Override public BinaryMetadata apply(BinaryMetadataHolder metaHolder) {
+                return metaHolder.metadata();
+            }
+        });
+    }
+
+    /**
+     * @return Binary metadata for specified type.
+     * @throws BinaryObjectException on error.
+     */
+    public BinaryMetadata binaryMetadata(int typeId) throws BinaryObjectException {
+        BinaryMetadataHolder hld = metadataLocCache.get(typeId);
+
+        return hld != null ? hld.metadata() : null;
+    }
+
     /** {@inheritDoc} */
     @Override public void saveMetadata(Collection<BinaryType> types, File dir) {
         try {
@@ -1433,8 +1469,10 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
         if (!dataBag.commonDataCollectedFor(BINARY_PROC.ordinal())) {
             Map<Integer, BinaryMetadataHolder> res = U.newHashMap(metadataLocCache.size());
 
-            for (Map.Entry<Integer,BinaryMetadataHolder> e : metadataLocCache.entrySet())
-                res.put(e.getKey(), e.getValue());
+            for (Map.Entry<Integer,BinaryMetadataHolder> e : metadataLocCache.entrySet()) {
+                if (!e.getValue().removing())
+                    res.put(e.getKey(), e.getValue());
+            }
 
             dataBag.addGridCommonData(BINARY_PROC.ordinal(), (Serializable) res);
         }
@@ -1536,6 +1574,42 @@ public class CacheObjectBinaryProcessorImpl extends GridProcessorAdapter impleme
         this.binaryMetadataFileStoreDir = binaryMetadataFileStoreDir;
     }
 
+    /** {@inheritDoc} */
+    @Override public void removeType(int typeId) {
+        BinaryMetadataHolder oldHld = metadataLocCache.get(typeId);
+
+        if (oldHld == null)
+            throw new IgniteException("Failed to remove metadata, type not found: " + typeId);
+
+        if (oldHld.removing())
+            throw new IgniteException("Failed to remove metadata, type is being removed: " + typeId);
+
+        if (!IgniteFeatures.allNodesSupports(ctx.discovery().allNodes(), IgniteFeatures.REMOVE_METADATA)) {
+            throw new IgniteException("Failed to remove metadata, " +
+                "all cluster nodes must support the remove type feature");
+        }
+
+        try {
+            GridFutureAdapter<MetadataUpdateResult> fut = transport.requestMetadataRemove(typeId);
+
+            MetadataUpdateResult res = fut.get();
+
+            if (res.rejected())
+                throw res.error();
+        }
+        catch (IgniteCheckedException e) {
+            IgniteCheckedException ex = e;
+
+            if (ctx.isStopping()) {
+                ex = new NodeStoppingException("Node is stopping.");
+
+                ex.addSuppressed(e);
+            }
+
+            throw new BinaryObjectException("Failed to remove metadata for type: " + typeId, ex);
+        }
+    }
+
     /** */
     @SuppressWarnings("PublicInnerClass")
     public static class TestBinaryContext extends BinaryContext {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveAcceptedMessage.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveAcceptedMessage.java
new file mode 100644
index 0000000..190e1c3
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveAcceptedMessage.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.processors.cache.binary;
+
+import org.apache.ignite.internal.managers.discovery.DiscoCache;
+import org.apache.ignite.internal.managers.discovery.DiscoveryCustomMessage;
+import org.apache.ignite.internal.managers.discovery.GridDiscoveryManager;
+import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Acknowledge message for {@link MetadataRemoveProposedMessage}: see its javadoc for detailed description of protocol.
+ *
+ * As discovery messaging doesn't guarantee that message makes only one pass across the cluster
+ * <b>MetadataRemoveAcceptedMessage</b> enables to mark it as duplicated so other nodes won't process it but skip.
+ */
+public class MetadataRemoveAcceptedMessage implements DiscoveryCustomMessage {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    private final IgniteUuid id = IgniteUuid.randomUuid();
+
+    /** */
+    private final int typeId;
+
+    /** */
+    private boolean duplicated;
+
+    /**
+     * @param typeId Type id.
+     */
+    MetadataRemoveAcceptedMessage(int typeId) {
+        this.typeId = typeId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteUuid id() {
+        return id;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public DiscoveryCustomMessage ackMessage() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isMutable() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public DiscoCache createDiscoCache(GridDiscoveryManager mgr,
+        AffinityTopologyVersion topVer, DiscoCache discoCache) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** */
+    public int typeId() {
+        return typeId;
+    }
+
+    /** */
+    public boolean duplicated() {
+        return duplicated;
+    }
+
+    /**
+     * @param duplicated duplicated flag.
+     */
+    public void duplicated(boolean duplicated) {
+        this.duplicated = duplicated;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(MetadataRemoveAcceptedMessage.class, this);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveProposedMessage.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveProposedMessage.java
new file mode 100644
index 0000000..eded65c
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/MetadataRemoveProposedMessage.java
@@ -0,0 +1,143 @@
+/*
+ * 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.binary;
+
+import java.util.UUID;
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.internal.binary.BinaryMetadata;
+import org.apache.ignite.internal.managers.discovery.DiscoCache;
+import org.apache.ignite.internal.managers.discovery.DiscoveryCustomMessage;
+import org.apache.ignite.internal.managers.discovery.GridDiscoveryManager;
+import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * <b>MetadataRemoveProposedMessage</b> and {@link MetadataRemoveAcceptedMessage} messages make a basis for
+ * discovery-based protocol for manage {@link BinaryMetadata metadata} describing objects in binary format
+ * stored in Ignite caches.
+ */
+public final class MetadataRemoveProposedMessage implements DiscoveryCustomMessage {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    private final IgniteUuid id = IgniteUuid.randomUuid();
+
+    /** Node UUID which initiated metadata update. */
+    private final UUID origNodeId;
+
+    /** Metadata type id. */
+    private final int typeId;
+
+    /** Message acceptance status. */
+    private ProposalStatus status = ProposalStatus.SUCCESSFUL;
+
+    /** Message received on coordinator. */
+    private boolean onCoordinator = true;
+
+    /** */
+    private BinaryObjectException err;
+
+    /**
+     * @param typeId Binary type ID.
+     * @param origNodeId ID of node requested update.
+     */
+    public MetadataRemoveProposedMessage(int typeId, UUID origNodeId) {
+        assert origNodeId != null;
+
+        this.origNodeId = origNodeId;
+
+        this.typeId = typeId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteUuid id() {
+        return id;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public DiscoveryCustomMessage ackMessage() {
+        return (status == ProposalStatus.SUCCESSFUL) ? new MetadataRemoveAcceptedMessage(typeId) : null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isMutable() {
+        return true;
+    }
+
+
+    /** {@inheritDoc} */
+    @Nullable @Override public DiscoCache createDiscoCache(GridDiscoveryManager mgr,
+        AffinityTopologyVersion topVer, DiscoCache discoCache) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @param err Error caused this update to be rejected.
+     */
+    void markRejected(BinaryObjectException err) {
+        status = ProposalStatus.REJECTED;
+        this.err = err;
+    }
+
+    /** */
+    boolean rejected() {
+        return status == ProposalStatus.REJECTED;
+    }
+
+    /** */
+    BinaryObjectException rejectionError() {
+        return err;
+    }
+
+    /** */
+    UUID origNodeId() {
+        return origNodeId;
+    }
+
+    /** */
+    public int typeId() {
+        return typeId;
+    }
+
+    /** */
+    public boolean isOnCoordinator() {
+        return onCoordinator;
+    }
+
+    /** */
+    public void setOnCoordinator(boolean onCoordinator) {
+        this.onCoordinator = onCoordinator;
+    }
+
+    /** Message acceptance status. */
+    private enum ProposalStatus {
+        /** */
+        SUCCESSFUL,
+
+        /** */
+        REJECTED
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(MetadataRemoveProposedMessage.class, this);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cacheobject/IgniteCacheObjectProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cacheobject/IgniteCacheObjectProcessor.java
index 62e15df..7ccfee0 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cacheobject/IgniteCacheObjectProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cacheobject/IgniteCacheObjectProcessor.java
@@ -338,4 +338,14 @@ public interface IgniteCacheObjectProcessor extends GridProcessor {
      * @throws IgniteException If failed.
      */
     public Object marshalToBinary(Object obj, boolean failIfUnregistered) throws IgniteException;
+
+    /**
+     * Remove registered binary type from grid.
+     *
+     * Attention: this is not safe feature, the grid must not contain binary objects
+     * with specified type, operations with specified type must not be processed on the cluster.
+     *
+     * @param typeId Type ID.
+     */
+    public void removeType(int typeId);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
index 36fe752..d467878 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
@@ -377,6 +377,76 @@ public class ClientListenerProcessor extends GridProcessorAdapter {
     }
 
     /**
+     *
+     */
+    public void closeAllSessions() {
+        Collection<? extends GridNioSession> sessions = srv.sessions();
+
+        for (GridNioSession ses : sessions) {
+            ClientListenerConnectionContext connCtx = ses.meta(CONN_CTX_META_KEY);
+
+            if (connCtx == null || ses.closeTime() != 0)
+                continue; // Skip non-initialized or closed session.
+
+            srv.close(ses);
+
+            if (log.isInfoEnabled()) {
+                log.info("Client session has been dropped: "
+                    + clientConnectionDescription(ses, connCtx));
+            }
+        }
+    }
+
+    /**
+     * Compose connection description string.
+     * @param ses Client's NIO session.
+     * @param ctx Client's connection context.
+     * @return connection description.
+     */
+    @SuppressWarnings("StringConcatenationInsideStringBufferAppend")
+    private static String clientConnectionDescription(
+        GridNioSession ses,
+        ClientListenerConnectionContext ctx
+    )
+    {
+        AuthorizationContext authCtx = ctx.authorizationContext();
+
+        StringBuilder sb = new StringBuilder();
+
+        if (ctx instanceof JdbcConnectionContext)
+            sb.append("JdbcClient [");
+        else if (ctx instanceof OdbcConnectionContext)
+            sb.append("OdbcClient [");
+        else
+            sb.append("ThinClient [");
+
+        InetSocketAddress rmtAddr = ses.remoteAddress();
+        InetSocketAddress locAddr = ses.localAddress();
+
+        assert rmtAddr != null;
+        assert locAddr != null;
+
+        String rmtAddrStr = rmtAddr.getHostString() + ":" + rmtAddr.getPort();
+        String locAddrStr = locAddr.getHostString() + ":" + locAddr.getPort();
+
+        String login;
+
+        if (authCtx != null)
+            login = authCtx.userName();
+        else if (ctx.securityContext() != null)
+            login = "@" + ctx.securityContext().subject().login();
+        else
+            login = "<anonymous>";
+
+        sb.append("id=" + ctx.connectionId());
+        sb.append(", user=").append(login);
+        sb.append(", rmtAddr=" + rmtAddrStr);
+        sb.append(", locAddr=" + locAddrStr);
+
+        return sb.append(']').toString();
+    }
+
+    /**
      * @return Server port.
      */
     public int port() {
@@ -426,11 +496,13 @@ public class ClientListenerProcessor extends GridProcessorAdapter {
                 cliConnCfg.setTcpNoDelay(sqlConnCfg.isTcpNoDelay());
                 cliConnCfg.setThreadPoolSize(sqlConnCfg.getThreadPoolSize());
 
-                U.warn(log, "Automatically converted deprecated " + SqlConnectorConfiguration.class.getSimpleName() +
+                U.warn(log, "Automatically converted deprecated "
+                    + SqlConnectorConfiguration.class.getSimpleName() +
                     " to " + ClientConnectorConfiguration.class.getSimpleName() + ".");
 
                 if (odbcCfg != null) {
-                    U.warn(log, "Deprecated " + OdbcConfiguration.class.getSimpleName() + " will be ignored because " +
+                    U.warn(log, "Deprecated " + OdbcConfiguration.class.getSimpleName() +
+                        " will be ignored because " +
                         SqlConnectorConfiguration.class.getSimpleName() + " is set.");
                 }
             }
@@ -534,19 +606,7 @@ public class ClientListenerProcessor extends GridProcessorAdapter {
 
         /** {@inheritDoc} */
         @Override public void dropAllConnections() {
-            Collection<? extends GridNioSession> sessions = srv.sessions();
-
-            for (GridNioSession ses : sessions) {
-                ClientListenerConnectionContext connCtx = ses.meta(CONN_CTX_META_KEY);
-
-                if (connCtx == null || ses.closeTime() != 0)
-                    continue; // Skip non-initialized or closed session.
-
-                srv.close(ses);
-
-                if (log.isInfoEnabled())
-                    log.info("Client session has been dropped: " + clientConnectionDescription(ses, connCtx));
-            }
+            closeAllSessions();
         }
 
         /** {@inheritDoc} */
@@ -562,57 +622,25 @@ public class ClientListenerProcessor extends GridProcessorAdapter {
                     continue;
 
                 if (ses.closeTime() != 0) {
-                    if (log.isDebugEnabled())
-                        log.debug("Client session is already closed: " + clientConnectionDescription(ses, connCtx));
+                    if (log.isDebugEnabled()) {
+                        log.debug("Client session is already closed: " +
+                            clientConnectionDescription(ses, connCtx));
+                    }
 
                     return false;
                 }
 
                 srv.close(ses);
 
-                if (log.isInfoEnabled())
-                    log.info("Client session has been dropped: " + clientConnectionDescription(ses, connCtx));
+                if (log.isInfoEnabled()) {
+                    log.info("Client session has been dropped: " +
+                        clientConnectionDescription(ses, connCtx));
+                }
 
                 return true;
             }
 
             return false;
         }
-
-        /**
-         * Compose connection description string.
-         * @param ses client NIO session.
-         * @param ctx client connection context.
-         * @return connection description
-         */
-        @SuppressWarnings("StringConcatenationInsideStringBufferAppend")
-        private String clientConnectionDescription(GridNioSession ses, ClientListenerConnectionContext ctx) {
-            AuthorizationContext authCtx = ctx.authorizationContext();
-
-            StringBuilder sb = new StringBuilder();
-
-            if (ctx instanceof JdbcConnectionContext)
-                sb.append("JdbcClient [");
-            else if (ctx instanceof OdbcConnectionContext)
-                sb.append("OdbcClient [");
-            else
-                sb.append("ThinClient [");
-
-            InetSocketAddress rmtAddr = ses.remoteAddress();
-            InetSocketAddress locAddr = ses.localAddress();
-
-            assert rmtAddr != null;
-            assert locAddr != null;
-
-            String rmtAddrStr = rmtAddr.getHostString() + ":" + rmtAddr.getPort();
-            String locAddrStr = locAddr.getHostString() + ":" + locAddr.getPort();
-
-            sb.append("id=" + ctx.connectionId());
-            sb.append(", user=").append(authCtx == null ? "<anonymous>" : authCtx.userName());
-            sb.append(", rmtAddr=" + rmtAddrStr);
-            sb.append(", locAddr=" + locAddrStr);
-
-            return sb.append(']').toString();
-        }
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/plugin/security/SecurityPermission.java b/modules/core/src/main/java/org/apache/ignite/plugin/security/SecurityPermission.java
index 86206c2..e062bc3 100644
--- a/modules/core/src/main/java/org/apache/ignite/plugin/security/SecurityPermission.java
+++ b/modules/core/src/main/java/org/apache/ignite/plugin/security/SecurityPermission.java
@@ -73,7 +73,10 @@ public enum SecurityPermission {
     CACHE_DESTROY,
 
     /** Join as server node permission. */
-    JOIN_AS_SERVER;
+    JOIN_AS_SERVER,
+
+    /** Administration operation with cluster metadata (REMOVE, UPDATE). */
+    ADMIN_METADATA_OPS;
 
     /** Enumerated values. */
     private static final SecurityPermission[] VALS = values();
diff --git a/modules/core/src/main/resources/META-INF/classnames.properties b/modules/core/src/main/resources/META-INF/classnames.properties
index fc82e59..1077ad9 100644
--- a/modules/core/src/main/resources/META-INF/classnames.properties
+++ b/modules/core/src/main/resources/META-INF/classnames.properties
@@ -361,6 +361,17 @@ org.apache.ignite.internal.commandline.cache.reset_lost_partitions.CacheResetLos
 org.apache.ignite.internal.commandline.cache.reset_lost_partitions.CacheResetLostPartitionsTask$CacheResetLostPartitionsJob
 org.apache.ignite.internal.commandline.cache.reset_lost_partitions.CacheResetLostPartitionsTaskArg
 org.apache.ignite.internal.commandline.cache.reset_lost_partitions.CacheResetLostPartitionsTaskResult
+org.apache.ignite.internal.commandline.meta.subcommands.MetadataAbstractSubCommand.VoidDto
+org.apache.ignite.internal.commandline.meta.tasks.MetadataListResult
+org.apache.ignite.internal.commandline.meta.tasks.MetadataInfoTask
+org.apache.ignite.internal.commandline.meta.tasks.MetadataInfoTask.MetadataListJob
+org.apache.ignite.internal.commandline.meta.tasks.MetadataMarshalled
+org.apache.ignite.internal.commandline.meta.tasks.MetadataRemoveTask
+org.apache.ignite.internal.commandline.meta.tasks.MetadataRemoveTask.MetadataRemoveJob
+org.apache.ignite.internal.commandline.meta.tasks.MetadataRemoveTask.DropAllThinSessionsJob
+org.apache.ignite.internal.commandline.meta.tasks.MetadataTypeArgs
+org.apache.ignite.internal.commandline.meta.tasks.MetadataUpdateTask
+org.apache.ignite.internal.commandline.meta.tasks.MetadataUpdateTask.MetadataUpdateJob
 org.apache.ignite.internal.compute.ComputeTaskCancelledCheckedException
 org.apache.ignite.internal.compute.ComputeTaskTimeoutCheckedException
 org.apache.ignite.internal.direct.DirectMessageReader$1
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java b/modules/core/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java
index ed72177..b8acdd0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java
@@ -630,6 +630,7 @@ public class CommandHandlerParsingTest {
             cmd == CommandList.SET_STATE ||
             cmd == CommandList.ENCRYPTION ||
             cmd == CommandList.KILL ||
-            cmd == CommandList.SNAPSHOT;
+            cmd == CommandList.SNAPSHOT ||
+            cmd == CommandList.METADATA;
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveTest.java
new file mode 100644
index 0000000..eb82266
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.binary;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.managers.discovery.DiscoveryCustomMessage;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.spi.discovery.DiscoverySpiCustomMessage;
+import org.apache.ignite.spi.discovery.DiscoverySpiListener;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class BinaryMetadataRemoveTest extends GridCommonAbstractTest {
+    /** Max retry cont. */
+    private static final int MAX_RETRY_CONT = 10;
+
+    /** */
+    private static final String CACHE_NAME = "cache";
+
+    /** */
+    private GridTestUtils.DiscoveryHook discoveryHook;
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        TcpDiscoverySpi discoSpi;
+
+        final GridTestUtils.DiscoveryHook discoveryHook0 = discoveryHook;
+
+        if (discoveryHook0 != null) {
+            discoSpi = new TcpDiscoverySpi() {
+                @Override public void setListener(@Nullable DiscoverySpiListener lsnr) {
+                    if (discoveryHook0 != null)
+                        super.setListener(GridTestUtils.DiscoverySpiListenerWrapper.wrap(lsnr, discoveryHook0));
+                }
+            };
+        }
+        else
+            discoSpi = new TcpDiscoverySpi();
+
+        cfg.setDiscoverySpi(discoSpi);
+
+        cfg.setCacheConfiguration(new CacheConfiguration().setName(CACHE_NAME));
+
+        return cfg;
+    }
+
+    /**
+     *
+     */
+    protected void startCluster() throws Exception {
+        startGrid("srv0");
+        startGrid("srv1");
+        startGrid("srv2");
+        startClientGrid("cli0");
+        startClientGrid("cli1");
+        startClientGrid("cli2");
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        startCluster();
+
+        discoveryHook = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /**
+     * Tests remove not existent type and checks the exception.
+     */
+    @Test
+    public void testRemoveNotExistentType() {
+        for (Ignite testNode : G.allGrids()) {
+            GridTestUtils.assertThrows(log, () -> {
+                    ((IgniteEx)testNode).context().cacheObjects().removeType(
+                        ((IgniteEx)testNode).context().cacheObjects().typeId("NotExistentType"));
+
+                    return null;
+                },
+                IgniteException.class, "Failed to remove metadata, type not found");
+        }
+    }
+
+    /**
+     * Tests remove type metadata at all nodes (coordinator, server, client).
+     */
+    @Test
+    public void testRemoveTypeOnNodes() throws Exception {
+        List<IgniteEx[]> testNodeSets = new ArrayList<>();
+
+        // Add all servers permutations to tests sets.
+        for (Ignite ign0 : G.allGrids()) {
+            for (Ignite ign1 : G.allGrids()) {
+                for (Ignite ign2 : G.allGrids()) {
+                    IgniteEx ignx0 = (IgniteEx)ign0;
+                    IgniteEx ignx1 = (IgniteEx)ign1;
+                    IgniteEx ignx2 = (IgniteEx)ign2;
+
+                    if (!ignx0.context().clientNode()
+                        && !ignx1.context().clientNode()
+                        && !ignx2.context().clientNode())
+                        testNodeSets.add(new IgniteEx[] {ignx0, ignx1, ignx2});
+                }
+            }
+        }
+
+        testNodeSets.add(new IgniteEx[] {grid("srv0"), grid("cli0"), grid("cli0")});
+        testNodeSets.add(new IgniteEx[] {grid("cli0"), grid("cli0"), grid("cli0")});
+        testNodeSets.add(new IgniteEx[] {grid("cli0"), grid("cli1"), grid("cli2")});
+
+        for (IgniteEx[] testNodeSet : testNodeSets) {
+            IgniteEx ignCreateType = testNodeSet[0];
+            IgniteEx ignRemoveType = testNodeSet[1];
+            IgniteEx ignRecreateType = testNodeSet[2];
+
+            log.info("+++ Check [createOn=" + ignCreateType.name() +
+                ", removeOn=" + ignRemoveType.name() + ", recreateOn=" + ignRecreateType.name());
+
+            BinaryObjectBuilder builder0 = ignCreateType.binary().builder("Type0");
+
+            builder0.setField("f", 1);
+            builder0.build();
+
+            delayIfClient(ignCreateType, ignRemoveType, ignRecreateType);
+
+            removeType(ignRemoveType, "Type0");
+
+            delayIfClient(ignCreateType, ignRemoveType, ignRecreateType);
+
+            BinaryObjectBuilder builder1 = ignRecreateType.binary().builder("Type0");
+            builder1.setField("f", "string");
+            builder1.build();
+
+            delayIfClient(ignCreateType, ignRemoveType, ignRecreateType);
+
+            // Remove type at the end of test case.
+            removeType(grid("srv0"), "Type0");
+
+            delayIfClient(ignCreateType, ignRemoveType, ignRecreateType);
+        }
+    }
+
+    /**
+     * Tests reject metadata update on coordinator when remove type is processed.
+     */
+    @Test
+    public void testChangeMetaWhenTypeRemoving() throws Exception {
+        final CyclicBarrier barrier0 = new CyclicBarrier(2);
+        final CyclicBarrier barrier1 = new CyclicBarrier(2);
+
+        AtomicBoolean hookMsgs = new AtomicBoolean(true);
+
+        discoveryHook = new GridTestUtils.DiscoveryHook() {
+            @Override public void beforeDiscovery(DiscoverySpiCustomMessage msg) {
+                if (!hookMsgs.get())
+                    return;
+
+                DiscoveryCustomMessage customMsg = msg == null ? null
+                    : (DiscoveryCustomMessage)IgniteUtils.field(msg, "delegate");
+
+                if (customMsg instanceof MetadataRemoveProposedMessage) {
+                    try {
+                        barrier0.await();
+
+                        barrier1.await();
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+
+        // Install discovery hoot at the node 'srv1'
+        stopGrid("srv1");
+        IgniteEx ign = startGrid("srv1");
+
+        discoveryHook = null;
+
+        // Move srv2 node an the end of the discovery circle.
+        stopGrid("srv2");
+        startGrid("srv2");
+
+        BinaryObjectBuilder builder0 = ign.binary().builder("Type0");
+
+        builder0.setField("f", 1);
+        builder0.build();
+
+        GridTestUtils.runAsync(() -> {
+            try {
+                removeType(ign, "Type0");
+            }
+            catch (Exception e) {
+                log.error("Unexpected exception", e);
+
+                fail("Unexpected exception.");
+            }
+        });
+
+        barrier0.await();
+
+        GridTestUtils.assertThrowsAnyCause(log, () -> {
+            BinaryObjectBuilder bld = grid("srv2").binary().builder("Type0");
+
+            bld.setField("f1", 1);
+
+            // Short delay guarantee that we go into update metadata before remove metadata continue processing.
+            GridTestUtils.runAsync(() -> {
+                try {
+                    U.sleep(200);
+
+                    hookMsgs.set(false);
+
+                    barrier1.await();
+                }
+                catch (Exception e) {
+                    // No-op.
+                }
+            });
+
+            bld.build();
+
+            return null;
+        }, BinaryObjectException.class, "The type is removing now");
+    }
+
+    /**
+     * @param ign Node to remove type.
+     * @param typeName Binary type name.
+     */
+    protected void removeType(IgniteEx ign, String typeName) throws Exception {
+        Exception err = null;
+
+        for (int i = 0; i < MAX_RETRY_CONT; ++i) {
+            try {
+                ign.context().cacheObjects().removeType(ign.context().cacheObjects().typeId(typeName));
+
+                err = null;
+
+                break;
+            }
+            catch (Exception e) {
+                err = e;
+
+                U.sleep(200);
+            }
+        }
+
+        if (err != null)
+            throw err;
+    }
+
+    /**
+     * Delay operation if an operation is executed on a client node.
+     *
+     * @param igns Tests nodes.
+     */
+    protected void delayIfClient(Ignite... igns) throws IgniteInterruptedCheckedException {
+        boolean isThereCli = Arrays.stream(igns).anyMatch(ign -> ((IgniteEx)ign).context().clientNode());
+
+        if (isThereCli)
+            U.sleep(500);
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveWithPersistenceTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveWithPersistenceTest.java
new file mode 100644
index 0000000..9133b1e
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataRemoveWithPersistenceTest.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.processors.cache.binary;
+
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.junit.Test;
+
+/**
+ */
+public class BinaryMetadataRemoveWithPersistenceTest extends BinaryMetadataRemoveTest {
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setDataStorageConfiguration(new DataStorageConfiguration()
+                .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+                    .setPersistenceEnabled(true)));
+    }
+
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        cleanPersistenceDir();
+
+        super.beforeTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void startCluster() throws Exception {
+        super.startCluster();
+
+        grid("srv0").cluster().active(true);
+    }
+
+    /**
+     * Remove type metadata and restart cluster.
+     */
+    @Test
+    public void testRemoveTypeAndClusterRestart() throws Exception {
+        for (String nodeName : new String[]{"srv0", "srv2", "cli0"}) {
+            log.info("+++ Check on " + nodeName);
+
+            BinaryObjectBuilder builder0 = grid(nodeName).binary().builder("Type0");
+
+            builder0.setField("f", 1);
+            builder0.build();
+
+            delayIfClient(grid(nodeName));
+
+            removeType(grid(nodeName), "Type0");
+
+            delayIfClient(grid(nodeName));
+
+            stopAllGrids();
+
+            startCluster();
+
+            BinaryObjectBuilder builder1 = grid(nodeName).binary().builder("Type0");
+            builder1.setField("f", "string");
+            builder1.build();
+
+            delayIfClient(grid(nodeName));
+
+            removeType(grid(nodeName), "Type0");
+
+            delayIfClient(grid(nodeName));
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsBinaryMetadataAsyncWritingTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsBinaryMetadataAsyncWritingTest.java
index 60524e0..575d03f 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsBinaryMetadataAsyncWritingTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsBinaryMetadataAsyncWritingTest.java
@@ -467,7 +467,7 @@ public class IgnitePdsBinaryMetadataAsyncWritingTest extends GridCommonAbstractT
 
         //internal map in BinaryMetadataFileStore with futures awaiting write operations
         Map map = GridTestUtils.getFieldValue(
-            (CacheObjectBinaryProcessorImpl)ig1.context().cacheObjects(), "metadataFileStore", "writer", "preparedWriteTasks");
+           ig1.context().cacheObjects(), "metadataFileStore", "writer", "preparedTasks");
 
         assertTrue(!map.isEmpty());
 
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 7e69f30..63f557b 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
@@ -60,6 +60,8 @@ import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataMoveLega
 import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataRegistrationCacheApiTest;
 import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataRegistrationCacheStoreTest;
 import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataRegistrationEntryProcessorTest;
+import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataRemoveTest;
+import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataRemoveWithPersistenceTest;
 import org.apache.ignite.internal.processors.cache.binary.BinaryMetadataUpdatesFlowTest;
 import org.apache.ignite.internal.processors.cache.binary.BinaryTxCacheLocalEntriesSelfTest;
 import org.apache.ignite.internal.processors.cache.binary.GridCacheBinaryObjectMetadataExchangeMultinodeTest;
@@ -85,6 +87,9 @@ import org.junit.runners.Suite;
  */
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
+    BinaryMetadataRemoveTest.class,
+    BinaryMetadataRemoveWithPersistenceTest.class,
+
     BinarySimpleNameTestPropertySelfTest.class,
 
     BinaryBasicIdMapperSelfTest.class,
diff --git a/modules/core/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java b/modules/core/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
index b963920..09722a1 100644
--- a/modules/core/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
@@ -23,6 +23,8 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -41,8 +43,6 @@ import java.util.logging.Logger;
 import java.util.logging.StreamHandler;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.Collection;
-import java.util.EnumMap;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
@@ -98,6 +98,7 @@ 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.commandline.CommandHandler.UTILITY_NAME;
 import static org.apache.ignite.internal.commandline.CommandList.BASELINE;
+import static org.apache.ignite.internal.commandline.CommandList.METADATA;
 import static org.apache.ignite.internal.commandline.CommandList.WAL;
 import static org.apache.ignite.internal.commandline.CommonArgParser.CMD_VERBOSE;
 import static org.apache.ignite.internal.commandline.OutputFormat.MULTI_LINE;
@@ -1592,6 +1593,7 @@ public class GridCommandHandlerClusterByClassTest extends GridCommandHandlerClus
         Map<CommandList, Collection<String>> cmdArgs = new EnumMap<>(CommandList.class);
 
         cmdArgs.put(WAL, asList("print", "delete"));
+        cmdArgs.put(METADATA, asList("help", "list"));
 
         String warning = String.format(
             "For use experimental command add %s=true to JVM_OPTS in %s",
@@ -1600,7 +1602,7 @@ public class GridCommandHandlerClusterByClassTest extends GridCommandHandlerClus
         );
 
         stream(CommandList.values()).filter(cmd -> cmd.command().experimental())
-            .peek(cmd -> assertTrue(cmdArgs.containsKey(cmd)))
+            .peek(cmd -> assertTrue("Not contains " + cmd, cmdArgs.containsKey(cmd)))
             .forEach(cmd -> cmdArgs.get(cmd).forEach(cmdArg -> {
                 assertEquals(EXIT_CODE_OK, execute(cmd.text(), cmdArg));
 
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 6048c94..9660e9c 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
@@ -125,6 +125,22 @@ This utility can do the following commands:
     Parameters:
       snapshot_name  - Snapshot name.
 
+  Print metadata command help:
+    control.(sh|bat) --meta help
+
+  Print list of binary metadata types:
+    control.(sh|bat) --meta list
+
+  Print detailed info about specified binary type (the type must be specified by type name or by type identifier):
+    control.(sh|bat) --meta details [--typeId <typeId>] [--typeName <typeName>]
+
+  Remove the metadata of the specified type (the type must be specified by type name or by type identifier) from cluster and saves the removed metadata to the specified file.
+If the file name isn't specified the output file name is: '<typeId>.bin'
+    control.(sh|bat) --meta remove [--typeId <typeId>] [--typeName <typeName>] [--out <fileName>]
+
+  Update cluster metadata from specified file (file name is required)
+    control.(sh|bat) --meta update --in <fileName>
+
 By default commands affecting the cluster require interactive confirmation.
 Use --yes option to disable it.
 
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 6048c94..9660e9c 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
@@ -125,6 +125,22 @@ This utility can do the following commands:
     Parameters:
       snapshot_name  - Snapshot name.
 
+  Print metadata command help:
+    control.(sh|bat) --meta help
+
+  Print list of binary metadata types:
+    control.(sh|bat) --meta list
+
+  Print detailed info about specified binary type (the type must be specified by type name or by type identifier):
+    control.(sh|bat) --meta details [--typeId <typeId>] [--typeName <typeName>]
+
+  Remove the metadata of the specified type (the type must be specified by type name or by type identifier) from cluster and saves the removed metadata to the specified file.
+If the file name isn't specified the output file name is: '<typeId>.bin'
+    control.(sh|bat) --meta remove [--typeId <typeId>] [--typeName <typeName>] [--out <fileName>]
+
+  Update cluster metadata from specified file (file name is required)
+    control.(sh|bat) --meta update --in <fileName>
+
 By default commands affecting the cluster require interactive confirmation.
 Use --yes option to disable it.
 
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingAndPersistenceTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingAndPersistenceTestSuite.java
index 9bca9c2..c5369f5 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingAndPersistenceTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingAndPersistenceTestSuite.java
@@ -26,6 +26,7 @@ import org.apache.ignite.util.GridCommandHandlerIndexingClusterByClassTest;
 import org.apache.ignite.util.GridCommandHandlerIndexingClusterByClassWithSSLTest;
 import org.apache.ignite.util.GridCommandHandlerIndexingTest;
 import org.apache.ignite.util.GridCommandHandlerIndexingWithSSLTest;
+import org.apache.ignite.util.GridCommandHandlerMetadataTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -42,7 +43,8 @@ import org.junit.runners.Suite;
     GridCommandHandlerIndexingCheckSizeTest.class,
     GridCommandHandlerCheckIndexesInlineSizeTest.class,
     StartCachesInParallelTest.class,
-    IoStatisticsBasicIndexSelfTest.class
+    IoStatisticsBasicIndexSelfTest.class,
+    GridCommandHandlerMetadataTest.class
 })
 public class IgniteCacheWithIndexingAndPersistenceTestSuite {
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/util/GridCommandHandlerMetadataTest.java b/modules/indexing/src/test/java/org/apache/ignite/util/GridCommandHandlerMetadataTest.java
new file mode 100644
index 0000000..d7a1d1e
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/util/GridCommandHandlerMetadataTest.java
@@ -0,0 +1,384 @@
+/*
+ * 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.util;
+
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.UUID;
+import org.apache.ignite.IgniteBinary;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.Ignition;
+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.client.ClientCacheConfiguration;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.internal.binary.BinarySchema;
+import org.apache.ignite.internal.binary.BinaryTypeImpl;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_INVALID_ARGUMENTS;
+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.testframework.GridTestUtils.assertContains;
+
+/**
+ * Checks command line metadata commands.
+ */
+public class GridCommandHandlerMetadataTest extends GridCommandHandlerClusterByClassAbstractTest {
+    /** File system. */
+    public static final FileSystem FS = FileSystems.getDefault();
+
+    /** Types count. */
+    private static final int TYPES_CNT = 10;
+
+    /**
+     * Check the command '--meta list'.
+     * Steps:
+     * - Creates binary types for a test (by BinaryObjectBuilder);
+     * - execute the command '--meta list'.
+     * - Check command output (must contains all created types).
+     */
+    @Test
+    public void testMetadataList() {
+        injectTestSystemOut();
+
+        for (int typeNum = 0; typeNum < TYPES_CNT; ++typeNum) {
+            BinaryObjectBuilder bob = crd.binary().builder("Type_" + typeNum);
+
+            for (int fldNum = 0; fldNum <= typeNum; ++fldNum)
+                bob.setField("fld_" + fldNum, 0);
+
+            bob.build();
+        }
+
+        assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
+
+        String out = testOut.toString();
+
+        for (int typeNum = 0; typeNum < TYPES_CNT; ++typeNum)
+            assertContains(log, out, "typeName=Type_" + typeNum);
+    }
+
+    /**
+     * Check the command '--meta details'.
+     * Steps:
+     * - Creates binary two types for a test (by BinaryObjectBuilder) with several fields and shemas;
+     * - execute the command '--meta details' for the type Type0 by name
+     * - check metadata print.
+     * - execute the command '--meta details' for the type Type0 by type ID on different formats.
+     * - check metadata print.
+     */
+    @Test
+    public void testMetadataDetails() {
+        injectTestSystemOut();
+
+        BinaryObjectBuilder bob0 = crd.binary().builder("TypeName0");
+        bob0.setField("fld0", 0);
+        bob0.build();
+
+        bob0 = crd.binary().builder("TypeName0");
+        bob0.setField("fld1", "0");
+        bob0.build();
+
+        bob0 = crd.binary().builder("TypeName0");
+        bob0.setField("fld0", 1);
+        bob0.setField("fld2", UUID.randomUUID());
+        BinaryObject bo0 = bob0.build();
+
+        BinaryObjectBuilder bob1 = crd.binary().builder("TypeName1");
+        bob1.setField("fld0", 0);
+        bob1.build();
+
+        bob1 = crd.binary().builder("TypeName1");
+        bob1.setField("fld0", 0);
+        bob1.setField("fld1", new Date());
+        bob1.setField("fld2", 0.1);
+        bob1.setField("fld3", new long[]{0, 1, 2, 3});
+
+        BinaryObject bo1 = bob1.build();
+
+        assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeName", "TypeName0"));
+        checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo0.type().typeId()));
+
+        assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeId",
+            "0x" + Integer.toHexString(crd.context().cacheObjects().typeId("TypeName1"))));
+        checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo1.type().typeId()));
+
+        assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeId",
+            Integer.toString(crd.context().cacheObjects().typeId("TypeName1"))));
+        checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo1.type().typeId()));
+    }
+
+    /**
+     * Check the command '--meta remove' and '--meta update' with invalid arguments.
+     * Steps:
+     * - executes 'remove' command without specified type.
+     * - checks error code and command output.
+     * - executes 'remove' command with '--out' option specified as the exist directory
+     *      ('--out' parameter must be a file name).
+     * - checks error code and command output.
+     * - executes 'update' command with '--in' option specified as the exist directory
+     *      ('--in' parameter must be a file name)
+     * - checks error code and command output.
+     */
+    @Test
+    public void testInvalidArguments() {
+        injectTestSystemOut();
+
+        String out;
+
+        assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove"));
+        out = testOut.toString();
+        assertContains(log, out, "Check arguments.");
+        assertContains(log, out, "Type to remove is not specified");
+        assertContains(log, out, "Please add one of the options: --typeName <type_name> or --typeId <type_id>");
+
+        assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove", "--typeId", "0", "--out", "target"));
+        out = testOut.toString();
+        assertContains(log, out, "Check arguments.");
+        assertContains(log, out, "Cannot write to output file target.");
+
+        assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "update", "--in", "target"));
+        out = testOut.toString();
+        assertContains(log, out, "Check arguments.");
+        assertContains(log, out, "Cannot read metadata from target");
+    }
+
+    /**
+     * Check the command '--meta remove' and '--meta update'.
+     * Steps:
+     * - creates the type 'Type0'.
+     * - removes the type by cmdline utility (store removed metadata to specified file).
+     * - checks that type removed (try to create Type0 with the same field and different type).
+     * - removes the new type 'Type0' by cmd line utility.
+     * - restores Typo0 from the file
+     * - checks restored type.
+     */
+    @Test
+    public void testRemoveUpdate() throws Exception {
+        injectTestSystemOut();
+
+        Path typeFile = FS.getPath("type0.bin");
+
+        try {
+            createType("Type0", 0);
+
+            // Type of the field cannot be changed.
+            GridTestUtils.assertThrowsAnyCause(log, () -> {
+                createType("Type0", "string");
+
+                return null;
+            }, BinaryObjectException.class, "Wrong value has been set");
+
+            assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
+                "--typeName", "Type0",
+                "--out", typeFile.toString()));
+
+            // New type must be created successfully after remove the type.
+            createType("Type0", "string");
+
+            assertEquals(EXIT_CODE_OK, execute("--meta", "remove", "--typeName", "Type0"));
+
+            // Restore the metadata from file.
+            assertEquals(EXIT_CODE_OK, execute("--meta", "update", "--in", typeFile.toString()));
+
+            // Type of the field cannot be changed.
+            GridTestUtils.assertThrowsAnyCause(log, () -> {
+                createType("Type0", "string");
+
+                return null;
+            }, BinaryObjectException.class, "Wrong value has been set");
+
+            createType("Type0", 1);
+
+            crd.context().cacheObjects().removeType(crd.context().cacheObjects().typeId("Type0"));
+
+            createType("Type0", "string");
+
+            // Restore the metadata from file.
+            assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--meta", "update", "--in", typeFile.toString()));
+
+            String out = testOut.toString();
+
+            assertContains(log, out, "Failed to execute metadata command='update'");
+            assertContains(log, out, "Type 'Type0' with typeId 110843958 has a " +
+                "different/incorrect type for field 'fld'.");
+            assertContains(log, out, "Expected 'String' but 'int' was provided. " +
+                "The type of an existing field can not be changed");
+        }
+        finally {
+            if (Files.exists(typeFile))
+                Files.delete(typeFile);
+        }
+    }
+
+    /**
+     * Check the all thin connections are dropped on the command '--meta remove' and '--meta update'.
+     * Steps:
+     * - opens thin client connection.
+     * - creates Type0 on thin client side.
+     * - removes type by cmd line util.
+     * - executes any command on thin client to detect disconnect.
+     * - creates Type0 on thin client side with other type of field (checks the type was removed).
+     */
+    @Test
+    public void testDropThinConnectionsOnRemove() throws Exception {
+        injectTestSystemOut();
+
+        Path typeFile = FS.getPath("type0.bin");
+
+        try (IgniteClient cli = Ignition.startClient(clientConfiguration())) {
+            createType(cli.binary(), "Type0", 1);
+
+            assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
+                "--typeName", "Type0",
+                "--out", typeFile.toString()));
+
+            // Executes command to check disconnect / reconnect.
+            GridTestUtils.assertThrows(log, () ->
+                    cli.createCache(new ClientCacheConfiguration().setName("test")),
+                Exception.class, null);
+
+            createType(cli.binary(), "Type0", "str");
+        }
+        finally {
+            if (Files.exists(typeFile))
+                Files.delete(typeFile);
+        }
+    }
+
+    /**
+     * Check the all thin connections are dropped on the command '--meta remove' and '--meta update'.
+     * Steps:
+     * - opens JDBC thin client connection.
+     * - executes: CREATE TABLE test(id INT PRIMARY KEY, objVal OTHER).
+     * - inserts the instance of the 'TestValue' class to the table.
+     * - removes the type 'TestValue' by cmd line.
+     * - executes any command on JDBC driver to detect disconnect.
+     * - checks metadata on client side. It must be empty.
+     */
+    @Test
+    public void testDropJdbcThinConnectionsOnRemove() throws Exception {
+        injectTestSystemOut();
+
+        Path typeFile = FS.getPath("type0.bin");
+
+        try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1")) {
+            try (final Statement stmt = conn.createStatement()) {
+                stmt.execute("CREATE TABLE test(id INT PRIMARY KEY, objVal OTHER)");
+
+                try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO test(id, objVal) VALUES (?, ?)")) {
+                    pstmt.setInt(1, 0);
+                    pstmt.setObject(2, new TestValue());
+
+                    pstmt.execute();
+                }
+
+                stmt.execute("DELETE FROM test WHERE id >= 0");
+            }
+
+            HashMap<Integer, BinaryType> metasOld = GridTestUtils.getFieldValue(conn, "metaHnd", "cache", "metas");
+
+            assertFalse(metasOld.isEmpty());
+
+            assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
+                "--typeName", TestValue.class.getName(),
+                "--out", typeFile.toString()));
+
+            // Executes any command to check disconnect.
+            GridTestUtils.assertThrows(log, () -> {
+                    try (Statement stmt = conn.createStatement()) {
+                        stmt.execute("SELECT * FROM test");
+                    }
+
+                    return null;
+                },
+                SQLException.class, "Failed to communicate with Ignite cluster");
+
+            HashMap<Integer, BinaryType> metas = GridTestUtils.getFieldValue(conn, "metaHnd", "cache", "metas");
+
+            assertNotSame(metasOld, metas);
+            assertTrue(metas.isEmpty());
+        }
+        finally {
+            if (Files.exists(typeFile))
+                Files.delete(typeFile);
+        }
+    }
+
+    /**
+     * @param t Binary type.
+     */
+    private void checkTypeDetails(@Nullable IgniteLogger log, String cmdOut, BinaryType t) {
+        assertContains(log, cmdOut, "typeId=" + "0x" + Integer.toHexString(t.typeId()).toUpperCase());
+        assertContains(log, cmdOut, "typeName=" + t.typeName());
+        assertContains(log, cmdOut, "Fields:");
+
+        for (String fldName : t.fieldNames())
+            assertContains(log, cmdOut, "name=" + fldName + ", type=" + t.fieldTypeName(fldName));
+
+        for (BinarySchema s : ((BinaryTypeImpl)t).metadata().schemas())
+            assertContains(log, cmdOut, "schemaId=0x" + Integer.toHexString(s.schemaId()).toUpperCase());
+    }
+
+    /**
+     * @param typeName Type name
+     * @param val Field value.
+     */
+    void createType(String typeName, Object val) {
+        createType(crd.binary(), typeName, val);
+    }
+
+    /**
+     * @param typeName Type name.
+     * @param val Field value.
+     */
+    void createType(IgniteBinary bin, String typeName, Object val) {
+        BinaryObjectBuilder bob = bin.builder(typeName);
+        bob.setField("fld", val);
+        bob.build();
+    }
+
+    /** */
+    private ClientConfiguration clientConfiguration() {
+        return new ClientConfiguration()
+            .setAddresses("127.0.0.1:10800");
+    }
+
+    /**
+     *
+     */
+    public static class TestValue {
+        /** */
+        public final int val = 3;
+    }
+}