thingsboard-aplcache

Changes

.gitignore 1(+1 -0)

dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java 182(+0 -182)

dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java 142(+0 -142)

docker/.env 25(+15 -10)

docker/cassandra/Makefile 10(+0 -10)

docker/cassandra-setup/Makefile 12(+0 -12)

docker/cassandra-upgrade/Makefile 12(+0 -12)

docker/cluster-mode-thirdparty.yml 45(+0 -45)

docker/k8s/cassandra.yaml 132(+0 -132)

docker/k8s/cassandra-setup.yaml 43(+0 -43)

docker/k8s/cassandra-upgrade.yaml 45(+0 -45)

docker/k8s/common.yaml 33(+0 -33)

docker/k8s/redis.yaml 93(+0 -93)

docker/k8s/tb.yaml 156(+0 -156)

docker/k8s/zookeeper.yaml 190(+0 -190)

docker/kafka.env 12(+12 -0)

docker/README.md 95(+95 -0)

docker/tb.env 26(+0 -26)

docker/tb/Makefile 12(+0 -12)

docker/tb/run-application.sh 59(+0 -59)

docker/tb-node.env 10(+10 -0)

docker/zookeeper/Dockerfile 71(+0 -71)

docker/zookeeper/Makefile 10(+0 -10)

docker/zookeeper/zk-gen-config.sh 153(+0 -153)

img/dashboard.gif 0(+0 -0)

msa/docker/.env 13(+0 -13)

msa/docker/docker-compose.yml 177(+0 -177)

msa/docker/tb-node.env 22(+0 -22)

msa/pom.xml 4(+2 -2)

msa/tb/pom.xml 370(+370 -0)

msa/tb/README.md 83(+83 -0)

msa/tb-node/pom.xml 69(+61 -8)

msa/web-ui/pom.xml 67(+59 -8)

ui/package.json 4(+2 -2)

ui/src/app/app.js 32(+18 -14)

Details

.gitignore 1(+1 -0)

diff --git a/.gitignore b/.gitignore
index 9039e8e..da8569b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,4 @@ pom.xml.versionsBackup
 **/Californium.properties
 **/.env
 .instance_id
+rebuild-docker.sh
diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 1ccfdc9..6550f58 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -15,7 +15,7 @@
         "resources": [],
         "templateHtml": "",
         "templateCss": "#container {\n    overflow: auto;\n}\n\n.tbDatasource-container {\n    margin: 5px;\n    padding: 8px;\n}\n\n.tbDatasource-title {\n    font-size: 1.200rem;\n    font-weight: 500;\n    padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n    width: 100%;\n    box-shadow: 0 0 10px #ccc;\n    border-collapse: collapse;\n    white-space: nowrap;\n    font-size: 1.000rem;\n    color: #757575;\n}\n\n.tbDatasource-table td {\n    position: relative;\n    border-top: 1px solid rgba(0, 0, 0, 0.12);\n    border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n    padding: 0px 18px;\n    box-sizing: border-box;\n}",
-        "controllerScript": "self.onInit = function() {\n    \n    self.ctx.datasourceTitleCells = [];\n    self.ctx.valueCells = [];\n    self.ctx.labelCells = [];\n    \n    for (var i=0; i < self.ctx.datasources.length; i++) {\n        var tbDatasource = self.ctx.datasources[i];\n\n        var datasourceId = 'tbDatasource' + i;\n        self.ctx.$container.append(\n            \"<div id='\" + datasourceId +\n            \"' class='tbDatasource-container'></div>\"\n        );\n\n        var datasourceContainer = $('#' + datasourceId,\n            self.ctx.$container);\n\n        datasourceContainer.append(\n            \"<div class='tbDatasource-title'>\" +\n            tbDatasource.name + \"</div>\"\n        );\n        \n        var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n        self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n        \n        var tableId = 'table' + i;\n        datasourceContainer.append(\n            \"<table id='\" + tableId +\n            \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n        );\n        var table = $('#' + tableId, self.ctx.$container);\n\n        for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n            var dataKey = tbDatasource.dataKeys[a];\n            var labelCellId = 'labelCell' + a;\n            var cellId = 'cell' + a;\n            table.append(\"<tr><td id='\" + labelCellId + \"'>\" + dataKey.label +\n                \"</td><td id='\" + cellId +\n                \"'></td></tr>\");\n            var labelCell = $('#' + labelCellId, table);\n            self.ctx.labelCells.push(labelCell);\n            var valueCell = $('#' + cellId, table);\n            self.ctx.valueCells.push(valueCell);\n        }\n    }    \n    \n    self.onResize();\n}\n\nself.onDataUpdated = function() {\n    for (var i = 0; i < self.ctx.valueCells.length; i++) {\n        var cellData = self.ctx.data[i];\n        console.log(self.ctx); //del\n        if (cellData && cellData.data && cellData.data.length > 0) {\n            var tvPair = cellData.data[cellData.data.length -\n                1];\n            var value = tvPair[1];\n            var textValue;\n            //toDo -> + IsNumber\n            \n            if (isNumber(value)) {\n                var decimals = self.ctx.decimals;\n                var units = self.ctx.units;\n                if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n                    decimals = cellData.dataKey.decimals;\n                }\n                if (cellData.dataKey.units) {\n                    units = cellData.dataKey.units;\n                }\n                txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n            } else {\n                txtValue = value;\n            }\n            self.ctx.valueCells[i].html(txtValue);\n        }\n    }\n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n}\n\nself.onResize = function() {\n    var datasoirceTitleFontSize = self.ctx.height/8;\n    if (self.ctx.width/self.ctx.height <= 1.5) {\n        datasoirceTitleFontSize = self.ctx.width/12;\n    }\n    datasoirceTitleFontSize = Math.min(datasoirceTitleFontSize, 20);\n    for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n        self.ctx.datasourceTitleCells[i].css('font-size', datasoirceTitleFontSize+'px');\n    }\n    var valueFontSize = self.ctx.height/9;\n    var labelFontSize = self.ctx.height/9;\n    if (self.ctx.width/self.ctx.height <= 1.5) {\n        valueFontSize = self.ctx.width/15;\n        labelFontSize = self.ctx.width/15;\n    }\n    valueFontSize = Math.min(valueFontSize, 18);\n    labelFontSize = Math.min(labelFontSize, 18);\n\n    for (i = 0; i < self.ctx.valueCells; i++) {\n        self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n        self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n        self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n        self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n        self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n        self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n    }    \n}\n\nself.onDestroy = function() {\n}\n",
+        "controllerScript": "self.onInit = function() {\n    \n    self.ctx.datasourceTitleCells = [];\n    self.ctx.valueCells = [];\n    self.ctx.labelCells = [];\n    \n    for (var i=0; i < self.ctx.datasources.length; i++) {\n        var tbDatasource = self.ctx.datasources[i];\n\n        var datasourceId = 'tbDatasource' + i;\n        self.ctx.$container.append(\n            \"<div id='\" + datasourceId +\n            \"' class='tbDatasource-container'></div>\"\n        );\n\n        var datasourceContainer = $('#' + datasourceId,\n            self.ctx.$container);\n\n        datasourceContainer.append(\n            \"<div class='tbDatasource-title'>\" +\n            tbDatasource.name + \"</div>\"\n        );\n        \n        var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n        self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n        \n        var tableId = 'table' + i;\n        datasourceContainer.append(\n            \"<table id='\" + tableId +\n            \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n        );\n        var table = $('#' + tableId, self.ctx.$container);\n\n        for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n            var dataKey = tbDatasource.dataKeys[a];\n            var labelCellId = 'labelCell' + a;\n            var cellId = 'cell' + a;\n            table.append(\"<tr><td id='\" + labelCellId + \"'>\" + dataKey.label +\n                \"</td><td id='\" + cellId +\n                \"'></td></tr>\");\n            var labelCell = $('#' + labelCellId, table);\n            self.ctx.labelCells.push(labelCell);\n            var valueCell = $('#' + cellId, table);\n            self.ctx.valueCells.push(valueCell);\n        }\n    }    \n    \n    self.onResize();\n}\n\nself.onDataUpdated = function() {\n    for (var i = 0; i < self.ctx.valueCells.length; i++) {\n        var cellData = self.ctx.data[i];\n        if (cellData && cellData.data && cellData.data.length > 0) {\n            var tvPair = cellData.data[cellData.data.length -\n                1];\n            var value = tvPair[1];\n            var textValue;\n            //toDo -> + IsNumber\n            \n            if (isNumber(value)) {\n                var decimals = self.ctx.decimals;\n                var units = self.ctx.units;\n                if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n                    decimals = cellData.dataKey.decimals;\n                }\n                if (cellData.dataKey.units) {\n                    units = cellData.dataKey.units;\n                }\n                txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n            } else {\n                txtValue = value;\n            }\n            self.ctx.valueCells[i].html(txtValue);\n        }\n    }\n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n}\n\nself.onResize = function() {\n    var datasoirceTitleFontSize = self.ctx.height/8;\n    if (self.ctx.width/self.ctx.height <= 1.5) {\n        datasoirceTitleFontSize = self.ctx.width/12;\n    }\n    datasoirceTitleFontSize = Math.min(datasoirceTitleFontSize, 20);\n    for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n        self.ctx.datasourceTitleCells[i].css('font-size', datasoirceTitleFontSize+'px');\n    }\n    var valueFontSize = self.ctx.height/9;\n    var labelFontSize = self.ctx.height/9;\n    if (self.ctx.width/self.ctx.height <= 1.5) {\n        valueFontSize = self.ctx.width/15;\n        labelFontSize = self.ctx.width/15;\n    }\n    valueFontSize = Math.min(valueFontSize, 18);\n    labelFontSize = Math.min(labelFontSize, 18);\n\n    for (i = 0; i < self.ctx.valueCells; i++) {\n        self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n        self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n        self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n        self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n        self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n        self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n    }    \n}\n\nself.onDestroy = function() {\n}\n",
         "settingsSchema": "{}",
         "dataKeySettingsSchema": "{}\n",
         "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}"
@@ -27,7 +27,7 @@
       "descriptor": {
         "type": "latest",
         "sizeX": 7.5,
-        "sizeY": 4.5,
+        "sizeY": 6.5,
         "resources": [],
         "templateHtml": "<tb-entities-table-widget \n    table-id=\"tableId\"\n    ctx=\"ctx\">\n</tb-entities-table-widget>",
         "templateCss": "",
diff --git a/application/src/main/data/upgrade/2.2.0/schema_update.sql b/application/src/main/data/upgrade/2.2.0/schema_update.sql
new file mode 100644
index 0000000..1832b79
--- /dev/null
+++ b/application/src/main/data/upgrade/2.2.0/schema_update.sql
@@ -0,0 +1,17 @@
+--
+-- Copyright © 2016-2018 The Thingsboard Authors
+--
+-- Licensed 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.
+--
+
+ALTER TABLE component_descriptor ADD UNIQUE (clazz);
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 5e19d69..091b504 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -67,6 +67,7 @@ import org.thingsboard.server.service.mail.MailExecutorService;
 import org.thingsboard.server.service.rpc.DeviceRpcService;
 import org.thingsboard.server.service.script.JsExecutorService;
 import org.thingsboard.server.service.script.JsInvokeService;
+import org.thingsboard.server.service.session.DeviceSessionCacheService;
 import org.thingsboard.server.service.state.DeviceStateService;
 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 import org.thingsboard.server.service.transport.RuleEngineTransportService;
@@ -201,6 +202,10 @@ public class ActorSystemContext {
     @Getter
     private DeviceStateService deviceStateService;
 
+    @Autowired
+    @Getter
+    private DeviceSessionCacheService deviceSessionCacheService;
+
     @Lazy
     @Autowired
     @Getter
@@ -254,6 +259,14 @@ public class ActorSystemContext {
     @Getter
     private boolean allowSystemMailService;
 
+    @Value("${transport.sessions.inactivity_timeout}")
+    @Getter
+    private long sessionInactivityTimeout;
+
+    @Value("${transport.sessions.report_timeout}")
+    @Getter
+    private long sessionReportTimeout;
+
     @Getter
     @Setter
     private ActorSystem actorSystem;
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index bd2a0f4..7b412b1 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -44,11 +44,19 @@ public class DeviceActor extends ContextAwareActor {
     }
 
     @Override
+    public void preStart() {
+        logger.debug("[{}][{}] Starting device actor.", processor.tenantId, processor.deviceId);
+        try {
+            processor.initSessionTimeout(context());
+            logger.debug("[{}][{}] Device actor started.", processor.tenantId, processor.deviceId);
+        } catch (Exception e) {
+            logger.error(e, "[{}][{}] Unknown failure", processor.tenantId, processor.deviceId);
+        }
+    }
+
+    @Override
     protected boolean process(TbActorMsg msg) {
         switch (msg.getMsgType()) {
-            case CLUSTER_EVENT_MSG:
-                processor.processClusterEventMsg((ClusterEventMsg) msg);
-                break;
             case TRANSPORT_TO_DEVICE_ACTOR_MSG:
                 processor.process(context(), (TransportToDeviceActorMsgWrapper) msg);
                 break;
@@ -73,6 +81,9 @@ public class DeviceActor extends ContextAwareActor {
             case DEVICE_ACTOR_CLIENT_SIDE_RPC_TIMEOUT_MSG:
                 processor.processClientSideRpcTimeout(context(), (DeviceActorClientSideRpcTimeoutMsg) msg);
                 break;
+            case SESSION_TIMEOUT_MSG:
+                processor.checkSessionsTimeout();
+                break;
             default:
                 return false;
         }
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 05902a6..a16bd78 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -41,7 +41,6 @@ import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
 import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
@@ -89,11 +88,11 @@ import java.util.stream.Collectors;
 /**
  * @author Andrew Shvayka
  */
-public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
+class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 
-    private final TenantId tenantId;
-    private final DeviceId deviceId;
-    private final Map<UUID, SessionInfo> sessions;
+    final TenantId tenantId;
+    final DeviceId deviceId;
+    private final Map<UUID, SessionInfoMetaData> sessions;
     private final Map<UUID, SessionInfo> attributeSubscriptions;
     private final Map<UUID, SessionInfo> rpcSubscriptions;
     private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
@@ -117,6 +116,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         this.toDeviceRpcPendingMap = new HashMap<>();
         this.toServerRpcPendingMap = new HashMap<>();
         initAttributes();
+        restoreSessions();
     }
 
     private void initAttributes() {
@@ -152,7 +152,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 
         if (request.isOneway() && sent) {
             logger.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
-            systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(msg.getMsg().getId(), msg.getServerAddress(), null, null));
+            systemContext.getDeviceRpcService().processResponseToServerSideRPCRequestFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
         } else {
             registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
         }
@@ -161,7 +161,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         } else {
             logger.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId());
         }
-
     }
 
     private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
@@ -174,8 +173,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
         if (requestMd != null) {
             logger.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
-            systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
-                    requestMd.getMsg().getServerAddress(), null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
+            systemContext.getDeviceRpcService().processResponseToServerSideRPCRequestFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+                    null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
         }
     }
 
@@ -202,12 +201,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 
     private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(ActorContext context, UUID sessionId, String nodeId, Set<Integer> sentOneWayIds) {
         return entry -> {
-            ToDeviceRpcRequestActorMsg requestActorMsg = entry.getValue().getMsg();
             ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
             ToDeviceRpcRequestBody body = request.getBody();
             if (request.isOneway()) {
                 sentOneWayIds.add(entry.getKey());
-                systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(request.getId(), requestActorMsg.getServerAddress(), null, null));
+                systemContext.getDeviceRpcService().processResponseToServerSideRPCRequestFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null));
             }
             ToDeviceRpcRequestMsg rpcRequest = ToDeviceRpcRequestMsg.newBuilder().setRequestId(
                     entry.getKey()).setMethodName(body.getMethod()).setParams(body.getParams()).build();
@@ -228,11 +226,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
         if (msg.hasPostAttributes()) {
             handlePostAttributesRequest(context, msg.getSessionInfo(), msg.getPostAttributes());
-            reportActivity();
+            reportLogicalDeviceActivity();
         }
         if (msg.hasPostTelemetry()) {
             handlePostTelemetryRequest(context, msg.getSessionInfo(), msg.getPostTelemetry());
-            reportActivity();
+            reportLogicalDeviceActivity();
         }
         if (msg.hasGetAttributes()) {
             handleGetAttributesRequest(context, msg.getSessionInfo(), msg.getGetAttributes());
@@ -242,11 +240,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
         if (msg.hasToServerRPCCallRequest()) {
             handleClientSideRPCRequest(context, msg.getSessionInfo(), msg.getToServerRPCCallRequest());
-            reportActivity();
+            reportLogicalDeviceActivity();
+        }
+        if (msg.hasSubscriptionInfo()) {
+            handleSessionActivity(context, msg.getSessionInfo(), msg.getSubscriptionInfo());
         }
     }
 
-    private void reportActivity() {
+    private void reportLogicalDeviceActivity() {
         systemContext.getDeviceStateService().onDeviceActivity(deviceId);
     }
 
@@ -400,35 +401,27 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
         boolean success = requestMd != null;
         if (success) {
-            systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
-                    requestMd.getMsg().getServerAddress(), responseMsg.getPayload(), null));
+            systemContext.getDeviceRpcService().processResponseToServerSideRPCRequestFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+                    responseMsg.getPayload(), null));
         } else {
             logger.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
         }
     }
 
-    void processClusterEventMsg(ClusterEventMsg msg) {
-//        if (!msg.isAdded()) {
-//            logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
-//            Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
-//                    .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
-//            attributeSubscriptions.entrySet().removeIf(filter);
-//            rpcSubscriptions.entrySet().removeIf(filter);
-//        }
-    }
-
     private void processSubscriptionCommands(ActorContext context, SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
         UUID sessionId = getSessionId(sessionInfo);
         if (subscribeCmd.getUnsubscribe()) {
             logger.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
             attributeSubscriptions.remove(sessionId);
         } else {
-            SessionInfo session = sessions.get(sessionId);
-            if (session == null) {
-                session = new SessionInfo(TransportProtos.SessionType.SYNC, sessionInfo.getNodeId());
+            SessionInfoMetaData sessionMD = sessions.get(sessionId);
+            if (sessionMD == null) {
+                sessionMD = new SessionInfoMetaData(new SessionInfo(TransportProtos.SessionType.SYNC, sessionInfo.getNodeId()));
             }
+            sessionMD.setSubscribedToAttributes(true);
             logger.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
-            attributeSubscriptions.put(sessionId, session);
+            attributeSubscriptions.put(sessionId, sessionMD.getSessionInfo());
+            dumpSessions();
         }
     }
 
@@ -442,20 +435,22 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
             logger.debug("[{}] Canceling rpc subscription for session [{}]", deviceId, sessionId);
             rpcSubscriptions.remove(sessionId);
         } else {
-            SessionInfo session = sessions.get(sessionId);
-            if (session == null) {
-                session = new SessionInfo(TransportProtos.SessionType.SYNC, sessionInfo.getNodeId());
+            SessionInfoMetaData sessionMD = sessions.get(sessionId);
+            if (sessionMD == null) {
+                sessionMD = new SessionInfoMetaData(new SessionInfo(TransportProtos.SessionType.SYNC, sessionInfo.getNodeId()));
             }
+            sessionMD.setSubscribedToRPC(true);
             logger.debug("[{}] Registering rpc subscription for session [{}]", deviceId, sessionId);
-            rpcSubscriptions.put(sessionId, session);
+            rpcSubscriptions.put(sessionId, sessionMD.getSessionInfo());
             sendPendingRequests(context, sessionId, sessionInfo);
+            dumpSessions();
         }
     }
 
     private void processSessionStateMsgs(SessionInfoProto sessionInfo, SessionEventMsg msg) {
         UUID sessionId = getSessionId(sessionInfo);
         if (msg.getEvent() == SessionEvent.OPEN) {
-            if(sessions.containsKey(sessionId)){
+            if (sessions.containsKey(sessionId)) {
                 logger.debug("[{}] Received duplicate session open event [{}]", deviceId, sessionId);
                 return;
             }
@@ -463,13 +458,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
             if (sessions.size() >= systemContext.getMaxConcurrentSessionsPerDevice()) {
                 UUID sessionIdToRemove = sessions.keySet().stream().findFirst().orElse(null);
                 if (sessionIdToRemove != null) {
-                    closeSession(sessionIdToRemove, sessions.remove(sessionIdToRemove));
+                    notifyTransportAboutClosedSession(sessionIdToRemove, sessions.remove(sessionIdToRemove));
                 }
             }
-            sessions.put(sessionId, new SessionInfo(TransportProtos.SessionType.ASYNC, sessionInfo.getNodeId()));
+            sessions.put(sessionId, new SessionInfoMetaData(new SessionInfo(TransportProtos.SessionType.ASYNC, sessionInfo.getNodeId())));
             if (sessions.size() == 1) {
                 reportSessionOpen();
             }
+            dumpSessions();
         } else if (msg.getEvent() == SessionEvent.CLOSED) {
             logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
             sessions.remove(sessionId);
@@ -478,21 +474,40 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
             if (sessions.isEmpty()) {
                 reportSessionClose();
             }
+            dumpSessions();
+        }
+    }
+
+    private void handleSessionActivity(ActorContext context, SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto subscriptionInfo) {
+        UUID sessionId = getSessionId(sessionInfo);
+        SessionInfoMetaData sessionMD = sessions.get(sessionId);
+        if (sessionMD != null) {
+            sessionMD.setLastActivityTime(subscriptionInfo.getLastActivityTime());
+            sessionMD.setSubscribedToAttributes(subscriptionInfo.getAttributeSubscription());
+            sessionMD.setSubscribedToRPC(subscriptionInfo.getRpcSubscription());
+            if (subscriptionInfo.getAttributeSubscription()) {
+                attributeSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
+            }
+            if (subscriptionInfo.getRpcSubscription()) {
+                rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
+            }
         }
+        dumpSessions();
     }
 
     void processCredentialsUpdate() {
-        sessions.forEach(this::closeSession);
+        sessions.forEach(this::notifyTransportAboutClosedSession);
         attributeSubscriptions.clear();
         rpcSubscriptions.clear();
+        dumpSessions();
     }
 
-    private void closeSession(UUID sessionId, SessionInfo sessionInfo) {
+    private void notifyTransportAboutClosedSession(UUID sessionId, SessionInfoMetaData sessionMd) {
         DeviceActorToTransportMsg msg = DeviceActorToTransportMsg.newBuilder()
                 .setSessionIdMSB(sessionId.getMostSignificantBits())
                 .setSessionIdLSB(sessionId.getLeastSignificantBits())
                 .setSessionCloseNotification(SessionCloseNotificationProto.getDefaultInstance()).build();
-        systemContext.getRuleEngineTransportService().process(sessionInfo.getNodeId(), msg);
+        systemContext.getRuleEngineTransportService().process(sessionMd.getSessionInfo().getNodeId(), msg);
     }
 
     void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) {
@@ -606,4 +621,75 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
         return builder.build();
     }
+
+    private void restoreSessions() {
+        logger.debug("[{}] Restoring sessions from cache", deviceId);
+        TransportProtos.DeviceSessionsCacheEntry sessionsDump = systemContext.getDeviceSessionCacheService().get(deviceId);
+        if (sessionsDump.getSerializedSize() == 0) {
+            logger.debug("[{}] No session information found", deviceId);
+            return;
+        }
+        for (TransportProtos.SessionSubscriptionInfoProto sessionSubscriptionInfoProto : sessionsDump.getSessionsList()) {
+            SessionInfoProto sessionInfoProto = sessionSubscriptionInfoProto.getSessionInfo();
+            UUID sessionId = getSessionId(sessionInfoProto);
+            SessionInfo sessionInfo = new SessionInfo(TransportProtos.SessionType.ASYNC, sessionInfoProto.getNodeId());
+            TransportProtos.SubscriptionInfoProto subInfo = sessionSubscriptionInfoProto.getSubscriptionInfo();
+            SessionInfoMetaData sessionMD = new SessionInfoMetaData(sessionInfo, subInfo.getLastActivityTime());
+            sessions.put(sessionId, sessionMD);
+            if (subInfo.getAttributeSubscription()) {
+                attributeSubscriptions.put(sessionId, sessionInfo);
+                sessionMD.setSubscribedToAttributes(true);
+            }
+            if (subInfo.getRpcSubscription()) {
+                rpcSubscriptions.put(sessionId, sessionInfo);
+                sessionMD.setSubscribedToRPC(true);
+            }
+            logger.debug("[{}] Restored session: {}", deviceId, sessionMD);
+        }
+        logger.debug("[{}] Restored sessions: {}, rpc subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
+    }
+
+    private void dumpSessions() {
+        logger.debug("[{}] Dumping sessions: {}, rpc subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
+        List<TransportProtos.SessionSubscriptionInfoProto> sessionsList = new ArrayList<>(sessions.size());
+        sessions.forEach((uuid, sessionMD) -> {
+            if (sessionMD.getSessionInfo().getType() == TransportProtos.SessionType.SYNC) {
+                return;
+            }
+            SessionInfo sessionInfo = sessionMD.getSessionInfo();
+            TransportProtos.SubscriptionInfoProto subscriptionInfoProto = TransportProtos.SubscriptionInfoProto.newBuilder()
+                    .setLastActivityTime(sessionMD.getLastActivityTime())
+                    .setAttributeSubscription(sessionMD.isSubscribedToAttributes())
+                    .setRpcSubscription(sessionMD.isSubscribedToRPC()).build();
+            TransportProtos.SessionInfoProto sessionInfoProto = TransportProtos.SessionInfoProto.newBuilder()
+                    .setSessionIdMSB(uuid.getMostSignificantBits())
+                    .setSessionIdLSB(uuid.getLeastSignificantBits())
+                    .setNodeId(sessionInfo.getNodeId()).build();
+            sessionsList.add(TransportProtos.SessionSubscriptionInfoProto.newBuilder()
+                    .setSessionInfo(sessionInfoProto)
+                    .setSubscriptionInfo(subscriptionInfoProto).build());
+            logger.debug("[{}] Dumping session: {}", deviceId, sessionMD);
+        });
+        systemContext.getDeviceSessionCacheService()
+                .put(deviceId, TransportProtos.DeviceSessionsCacheEntry.newBuilder()
+                        .addAllSessions(sessionsList).build());
+    }
+
+    void initSessionTimeout(ActorContext context) {
+        schedulePeriodicMsgWithDelay(context, SessionTimeoutCheckMsg.instance(), systemContext.getSessionInactivityTimeout(), systemContext.getSessionInactivityTimeout());
+    }
+
+    void checkSessionsTimeout() {
+        long expTime = System.currentTimeMillis() - systemContext.getSessionInactivityTimeout();
+        Map<UUID, SessionInfoMetaData> sessionsToRemove = sessions.entrySet().stream().filter(kv -> kv.getValue().getLastActivityTime() < expTime).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        sessionsToRemove.forEach((sessionId, sessionMD) -> {
+            sessions.remove(sessionId);
+            rpcSubscriptions.remove(sessionId);
+            attributeSubscriptions.remove(sessionId);
+            notifyTransportAboutClosedSession(sessionId, sessionMD);
+        });
+        if (!sessionsToRemove.isEmpty()) {
+            dumpSessions();
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
index 43ae592..fe09077 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
@@ -25,4 +25,5 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionType;
 public class SessionInfo {
     private final SessionType type;
     private final String nodeId;
+    private long lastActivityTime;
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java
new file mode 100644
index 0000000..dd08394
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfoMetaData.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.device;
+
+import lombok.Data;
+import org.thingsboard.server.gen.transport.TransportProtos.SessionType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+class SessionInfoMetaData {
+    private final SessionInfo sessionInfo;
+    private long lastActivityTime;
+    private boolean subscribedToAttributes;
+    private boolean subscribedToRPC;
+
+    SessionInfoMetaData(SessionInfo sessionInfo) {
+        this(sessionInfo, System.currentTimeMillis());
+    }
+
+    SessionInfoMetaData(SessionInfo sessionInfo, long lastActivityTime) {
+        this.sessionInfo = sessionInfo;
+        this.lastActivityTime = lastActivityTime;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java
new file mode 100644
index 0000000..d9172ae
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionTimeoutCheckMsg.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.device;
+
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+
+/**
+ * Created by ashvayka on 29.10.18.
+ */
+public class SessionTimeoutCheckMsg implements TbActorMsg {
+
+    private static final SessionTimeoutCheckMsg INSTANCE = new SessionTimeoutCheckMsg();
+
+    private SessionTimeoutCheckMsg() {
+    }
+
+    public static SessionTimeoutCheckMsg instance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_TIMEOUT_MSG;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
index 14bb636..1eba066 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -32,7 +32,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
     private final ActorRef manager;
     private final ActorRef self;
 
-    public BasicRpcSessionListener(ActorService service, ActorRef manager, ActorRef self) {
+    BasicRpcSessionListener(ActorService service, ActorRef manager, ActorRef self) {
         this.service = service;
         this.manager = manager;
         this.self = self;
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 608a77f..31320ce 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -45,7 +45,7 @@ public class RpcManagerActor extends ContextAwareActor {
 
     private final ServerAddress instance;
 
-    public RpcManagerActor(ActorSystemContext systemContext) {
+    RpcManagerActor(ActorSystemContext systemContext) {
         super(systemContext);
         this.sessionActors = new HashMap<>();
         this.pendingMsgs = new HashMap<>();
@@ -55,7 +55,6 @@ public class RpcManagerActor extends ContextAwareActor {
                 .filter(otherServer -> otherServer.getServerAddress().compareTo(instance) > 0)
                 .forEach(otherServer -> onCreateSessionRequest(
                         new RpcSessionCreateRequestMsg(UUID.randomUUID(), otherServer.getServerAddress(), null)));
-
     }
 
     @Override
@@ -104,10 +103,10 @@ public class RpcManagerActor extends ContextAwareActor {
             ServerAddress address = new ServerAddress(msg.getServerAddress().getHost(), msg.getServerAddress().getPort(), ServerType.CORE);
             SessionActorInfo session = sessionActors.get(address);
             if (session != null) {
-                log.debug("{} Forwarding msg to session actor", address);
+                log.debug("{} Forwarding msg to session actor: {}", address, msg);
                 session.getActor().tell(msg, ActorRef.noSender());
             } else {
-                log.debug("{} Storing msg to pending queue", address);
+                log.debug("{} Storing msg to pending queue: {}", address, msg);
                 Queue<ClusterAPIProtos.ClusterMessage> queue = pendingMsgs.get(address);
                 if (queue == null) {
                     queue = new LinkedList<>();
@@ -117,7 +116,7 @@ public class RpcManagerActor extends ContextAwareActor {
                 queue.add(msg);
             }
         } else {
-            logger.warning("Cluster msg doesn't have set Server Address [{}]", msg);
+            logger.warning("Cluster msg doesn't have server address [{}]", msg);
         }
     }
 
@@ -162,9 +161,9 @@ public class RpcManagerActor extends ContextAwareActor {
     }
 
     private void onSessionClose(boolean reconnect, ServerAddress remoteAddress) {
-        log.debug("[{}] session closed. Should reconnect: {}", remoteAddress, reconnect);
+        log.info("[{}] session closed. Should reconnect: {}", remoteAddress, reconnect);
         SessionActorInfo sessionRef = sessionActors.get(remoteAddress);
-        if (context().sender() != null && context().sender().equals(sessionRef.actor)) {
+        if (sessionRef != null && context().sender() != null && context().sender().equals(sessionRef.actor)) {
             sessionActors.remove(remoteAddress);
             pendingMsgs.remove(remoteAddress);
             if (reconnect) {
@@ -182,18 +181,18 @@ public class RpcManagerActor extends ContextAwareActor {
 
     private void register(ServerAddress remoteAddress, UUID uuid, ActorRef sender) {
         sessionActors.put(remoteAddress, new SessionActorInfo(uuid, sender));
-        log.debug("[{}][{}] Registering session actor.", remoteAddress, uuid);
+        log.info("[{}][{}] Registering session actor.", remoteAddress, uuid);
         Queue<ClusterAPIProtos.ClusterMessage> data = pendingMsgs.remove(remoteAddress);
         if (data != null) {
-            log.debug("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
+            log.info("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
             data.forEach(msg -> sender.tell(new RpcSessionTellMsg(msg), ActorRef.noSender()));
         } else {
-            log.debug("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
+            log.info("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
         }
     }
 
     private ActorRef createSessionActor(RpcSessionCreateRequestMsg msg) {
-        log.debug("[{}] Creating session actor.", msg.getMsgUid());
+        log.info("[{}] Creating session actor.", msg.getMsgUid());
         ActorRef actor = context().actorOf(
                 Props.create(new RpcSessionActor.ActorCreator(systemContext, msg.getMsgUid())).withDispatcher(DefaultActorService.RPC_DISPATCHER_NAME));
         actor.tell(msg, context().self());
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
index c9cf869..86509ca 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.actors.rpc;
 import akka.event.Logging;
 import akka.event.LoggingAdapter;
 import io.grpc.Channel;
+import io.grpc.ManagedChannel;
 import io.grpc.ManagedChannelBuilder;
 import io.grpc.stub.StreamObserver;
 import org.thingsboard.server.actors.ActorSystemContext;
@@ -88,8 +89,8 @@ public class RpcSessionActor extends ContextAwareActor {
             systemContext.getRpcService().onSessionCreated(msg.getMsgUid(), session.getInputStream());
         } else {
             // Client session
-            Channel channel = ManagedChannelBuilder.forAddress(remoteServer.getHost(), remoteServer.getPort()).usePlaintext(true).build();
-            session = new GrpcSession(remoteServer, listener);
+            ManagedChannel channel = ManagedChannelBuilder.forAddress(remoteServer.getHost(), remoteServer.getPort()).usePlaintext().build();
+            session = new GrpcSession(remoteServer, listener, channel);
             session.initInputStream();
 
             ClusterRpcServiceGrpc.ClusterRpcServiceStub stub = ClusterRpcServiceGrpc.newStub(channel);
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index 4e779d4..0baecea 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.actors.ruleChain;
 
 import akka.actor.ActorRef;
 import com.datastax.driver.core.utils.UUIDs;
+import org.springframework.util.StringUtils;
 import org.thingsboard.rule.engine.api.ListeningExecutor;
 import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
@@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
 import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.cluster.ServerType;
 import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
@@ -232,16 +235,22 @@ class DefaultTbContext implements TbContext {
         return new RuleEngineRpcService() {
             @Override
             public void sendRpcReply(DeviceId deviceId, int requestId, String body) {
-                mainCtx.getDeviceRpcService().sendRpcReplyToDevice(nodeCtx.getTenantId(), deviceId, requestId, body);
+                mainCtx.getDeviceRpcService().sendReplyToRpcCallFromDevice(nodeCtx.getTenantId(), deviceId, requestId, body);
             }
 
             @Override
             public void sendRpcRequest(RuleEngineDeviceRpcRequest src, Consumer<RuleEngineDeviceRpcResponse> consumer) {
                 ToDeviceRpcRequest request = new ToDeviceRpcRequest(src.getRequestUUID(), nodeCtx.getTenantId(), src.getDeviceId(),
                         src.isOneway(), src.getExpirationTime(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()));
-                mainCtx.getDeviceRpcService().processRpcRequestToDevice(request, response -> {
+                mainCtx.getDeviceRpcService().forwardServerSideRPCRequestToDeviceActor(request, response -> {
                     if (src.isRestApiCall()) {
-                        mainCtx.getDeviceRpcService().processRestAPIRpcResponseFromRuleEngine(response);
+                        ServerAddress requestOriginAddress;
+                        if (!StringUtils.isEmpty(src.getOriginHost())) {
+                            requestOriginAddress = new ServerAddress(src.getOriginHost(), src.getOriginPort(), ServerType.CORE);
+                        } else {
+                            requestOriginAddress = mainCtx.getRoutingService().getCurrentServer();
+                        }
+                        mainCtx.getDeviceRpcService().processResponseToServerSideRPCRequestFromRuleEngine(requestOriginAddress, response);
                     }
                     consumer.accept(RuleEngineDeviceRpcResponse.builder()
                             .deviceId(src.getDeviceId())
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index f7e80e4..08d1dd8 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -35,5 +35,4 @@ public interface ActorService extends SessionMsgProcessor, RpcMsgListener, Disco
 
     void onDeviceNameOrTypeUpdate(TenantId tenantId, DeviceId deviceId, String deviceName, String deviceType);
 
-    void onMsg(ServiceToRuleEngineMsg serviceToRuleEngineMsg);
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index c2e8fd0..85b8943 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -42,7 +42,6 @@ import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
-import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
@@ -66,10 +65,7 @@ public class DefaultActorService implements ActorService {
     public static final String APP_DISPATCHER_NAME = "app-dispatcher";
     public static final String CORE_DISPATCHER_NAME = "core-dispatcher";
     public static final String SYSTEM_RULE_DISPATCHER_NAME = "system-rule-dispatcher";
-    public static final String SYSTEM_PLUGIN_DISPATCHER_NAME = "system-plugin-dispatcher";
     public static final String TENANT_RULE_DISPATCHER_NAME = "rule-dispatcher";
-    public static final String TENANT_PLUGIN_DISPATCHER_NAME = "plugin-dispatcher";
-    public static final String SESSION_DISPATCHER_NAME = "session-dispatcher";
     public static final String RPC_DISPATCHER_NAME = "rpc-dispatcher";
 
     @Autowired
@@ -162,11 +158,6 @@ public class DefaultActorService implements ActorService {
         appActor.tell(new SendToClusterMsg(deviceId, msg), ActorRef.noSender());
     }
 
-    @Override
-    public void onMsg(ServiceToRuleEngineMsg msg) {
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
     public void broadcast(ToAllNodesMsg msg) {
         actorContext.getEncodingService().encode(msg);
         rpcService.broadcast(new RpcBroadcastMsg(ClusterAPIProtos.ClusterMessage
@@ -186,9 +177,9 @@ public class DefaultActorService implements ActorService {
     @Override
     public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) {
         ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort(), source.getServerType());
-        log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
         if (log.isDebugEnabled()) {
-            log.info("MSG: ", msg);
+            log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
+            log.info("MSG: {}", msg);
         }
         switch (msg.getMessageType()) {
             case CLUSTER_ACTOR_MESSAGE:
@@ -222,7 +213,7 @@ public class DefaultActorService implements ActorService {
                 actorContext.getTsSubService().onRemoteTsUpdate(serverAddress, msg.getPayload().toByteArray());
                 break;
             case CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE:
-                actorContext.getDeviceRpcService().processRemoteResponseFromDevice(serverAddress, msg.getPayload().toByteArray());
+                actorContext.getDeviceRpcService().processResponseToServerSideRPCRequestFromRemoteServer(serverAddress, msg.getPayload().toByteArray());
                 break;
             case CLUSTER_DEVICE_STATE_SERVICE_MESSAGE:
                 actorContext.getDeviceStateService().onRemoteMsg(serverAddress, msg.getPayload().toByteArray());
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 8864486..c809782 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -40,43 +40,31 @@ public abstract class AbstractContextAwareMsgProcessor {
         this.logger = logger;
     }
 
-    protected ActorRef getAppActor() {
-        return systemContext.getAppActor();
-    }
-
-    protected Scheduler getScheduler() {
+    private Scheduler getScheduler() {
         return systemContext.getScheduler();
     }
 
-    protected ExecutionContextExecutor getSystemDispatcher() {
+    private ExecutionContextExecutor getSystemDispatcher() {
         return systemContext.getActorSystem().dispatcher();
     }
 
     protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs) {
-        schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs, ctx.self());
+        schedulePeriodicMsgWithDelay(msg, delayInMs, periodInMs, ctx.self());
     }
 
-    protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs, ActorRef target) {
+    private void schedulePeriodicMsgWithDelay(Object msg, long delayInMs, long periodInMs, ActorRef target) {
         logger.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs);
         getScheduler().schedule(Duration.create(delayInMs, TimeUnit.MILLISECONDS), Duration.create(periodInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
     }
 
-
     protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs) {
-        scheduleMsgWithDelay(ctx, msg, delayInMs, ctx.self());
+        scheduleMsgWithDelay(msg, delayInMs, ctx.self());
     }
 
-    protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, ActorRef target) {
+    private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
         logger.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
         getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
     }
 
-    @Data
-    @AllArgsConstructor
-    private static class ComponentConfiguration {
-        private final String clazz;
-        private final String name;
-        private final String configuration;
-    }
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index 347483a..721d828 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -108,7 +108,7 @@ public class TenantActor extends RuleChainManagerActor {
     @Override
     protected void broadcast(Object msg) {
         super.broadcast(msg);
-        deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+//        deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
@@ -127,7 +127,6 @@ public class TenantActor extends RuleChainManagerActor {
         ruleChainManager.getOrCreateActor(context(), msg.getRuleChainId()).tell(msg, self());
     }
 
-
     private void onToDeviceActorMsg(DeviceAwareMsg msg) {
         getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 582bbed..be1b6db 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
@@ -673,7 +674,7 @@ public abstract class BaseController {
                 TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, entityId, metaData, TbMsgDataType.JSON
                         , json.writeValueAsString(entityNode)
                         , null, null, 0L);
-                actorService.onMsg(new ServiceToRuleEngineMsg(user.getTenantId(), tbMsg));
+                actorService.onMsg(new SendToClusterMsg(entityId, new ServiceToRuleEngineMsg(user.getTenantId(), tbMsg)));
             } catch (Exception e) {
                 log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
             }
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
index 9b7ffd2..2a9c073 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
@@ -29,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestController;
-import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.EntitySubtype;
@@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
 import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
-import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityViewId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -47,7 +45,6 @@ import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.service.security.model.SecurityUser;
@@ -174,7 +171,7 @@ public class EntityViewController extends BaseController {
             EntityView entityView = checkEntityViewId(entityViewId);
             entityViewService.deleteEntityView(entityViewId);
             logEntityAction(entityViewId, entityView, entityView.getCustomerId(),
-                    ActionType.DELETED,null, strEntityViewId);
+                    ActionType.DELETED, null, strEntityViewId);
         } catch (Exception e) {
             logEntityAction(emptyId(EntityType.ENTITY_VIEW),
                     null,
@@ -185,10 +182,23 @@ public class EntityViewController extends BaseController {
     }
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET)
+    @ResponseBody
+    public EntityView getTenantEntityView(
+            @RequestParam String entityViewName) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST)
     @ResponseBody
     public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId,
-                                             @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
+                                                 @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
         checkParameter(CUSTOMER_ID, strCustomerId);
         checkParameter(ENTITY_VIEW_ID, strEntityViewId);
         try {
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
index 6c37614..ef09a7a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -49,9 +49,11 @@ import org.thingsboard.server.common.data.kv.Aggregation;
 import org.thingsboard.server.common.data.kv.AttributeKey;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
 import org.thingsboard.server.common.data.kv.DoubleDataEntry;
 import org.thingsboard.server.common.data.kv.KvEntry;
 import org.thingsboard.server.common.data.kv.LongDataEntry;
@@ -60,12 +62,10 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
-import org.thingsboard.server.dao.attributes.AttributesService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
 import org.thingsboard.server.service.security.AccessValidator;
 import org.thingsboard.server.service.security.model.SecurityUser;
 import org.thingsboard.server.service.telemetry.AttributeData;
-import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 import org.thingsboard.server.service.telemetry.TsData;
 import org.thingsboard.server.service.telemetry.exception.InvalidParametersException;
 import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
@@ -250,6 +250,60 @@ public class TelemetryController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> deleteEntityTimeseries(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @RequestParam(name = "keys") String keysStr,
+                                                                 @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys,
+                                                                 @RequestParam(name = "startTs", required = false) Long startTs,
+                                                                 @RequestParam(name = "endTs", required = false) Long endTs,
+                                                                 @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted);
+    }
+
+    private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
+                                                            Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException {
+        List<String> keys = toKeysList(keysStr);
+        if (keys.isEmpty()) {
+            return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
+        }
+        SecurityUser user = getCurrentUser();
+
+        long deleteFromTs;
+        long deleteToTs;
+        if (deleteAllDataForKeys) {
+            deleteFromTs = 0L;
+            deleteToTs = System.currentTimeMillis();
+        } else {
+            deleteFromTs = startTs;
+            deleteToTs = endTs;
+        }
+
+        return accessValidator.validateEntityAndCallback(user, entityIdStr, (result, entityId) -> {
+            List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
+            for (String key : keys) {
+                deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted));
+            }
+
+            ListenableFuture<List<Void>> future = tsService.remove(entityId, deleteTsKvQueries);
+            Futures.addCallback(future, new FutureCallback<List<Void>>() {
+                @Override
+                public void onSuccess(@Nullable List<Void> tmp) {
+                    logTimeseriesDeleted(user, entityId, keys, null);
+                    result.setResult(new ResponseEntity<>(HttpStatus.OK));
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    logTimeseriesDeleted(user, entityId, keys, t);
+                    result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                }
+            }, executor);
+        });
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
     @ResponseBody
     public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr,
@@ -506,6 +560,15 @@ public class TelemetryController extends BaseController {
         };
     }
 
+    private void logTimeseriesDeleted(SecurityUser user, EntityId entityId, List<String> keys, Throwable e) {
+        try {
+            logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.TIMESERIES_DELETED, toException(e),
+                    keys);
+        } catch (ThingsboardException te) {
+            log.warn("Failed to log timeseries delete", te);
+        }
+    }
+
     private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
         try {
             logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e),
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index 45eb66a..2486cd8 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -101,6 +101,10 @@ public class ThingsboardInstallService {
                         log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
 
                         databaseUpgradeService.upgradeDatabase("2.1.1");
+                    case "2.1.3":
+                        log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ...");
+
+                        databaseUpgradeService.upgradeDatabase("2.1.3");
 
                         log.info("Updating system data...");
 
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
index 55cf52c..dd0a995 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
@@ -15,6 +15,8 @@
  */
 package org.thingsboard.server.service.cluster.rpc;
 
+import io.grpc.Channel;
+import io.grpc.ManagedChannel;
 import io.grpc.stub.StreamObserver;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
@@ -34,6 +36,7 @@ public final class GrpcSession implements Closeable {
     private final UUID sessionId;
     private final boolean client;
     private final GrpcSessionListener listener;
+    private final ManagedChannel channel;
     private StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream;
     private StreamObserver<ClusterAPIProtos.ClusterMessage> outputStream;
 
@@ -41,10 +44,10 @@ public final class GrpcSession implements Closeable {
     private ServerAddress remoteServer;
 
     public GrpcSession(GrpcSessionListener listener) {
-        this(null, listener);
+        this(null, listener, null);
     }
 
-    public GrpcSession(ServerAddress remoteServer, GrpcSessionListener listener) {
+    public GrpcSession(ServerAddress remoteServer, GrpcSessionListener listener, ManagedChannel channel) {
         this.sessionId = UUID.randomUUID();
         this.listener = listener;
         if (remoteServer != null) {
@@ -54,6 +57,7 @@ public final class GrpcSession implements Closeable {
         } else {
             this.client = false;
         }
+        this.channel = channel;
     }
 
     public void initInputStream() {
@@ -105,5 +109,8 @@ public final class GrpcSession implements Closeable {
         } catch (IllegalStateException e) {
             log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage());
         }
+        if (channel != null) {
+            channel.shutdownNow();
+        }
     }
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index f3ceed2..545baf8 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -30,7 +30,6 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
 import org.thingsboard.rule.engine.api.NodeDefinition;
 import org.thingsboard.rule.engine.api.RuleNode;
 import org.thingsboard.rule.engine.api.TbRelationTypes;
-import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
@@ -52,6 +51,7 @@ import java.util.Set;
 @Slf4j
 public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
 
+    public static final int MAX_OPTIMISITC_RETRIES = 3;
     @Value("${plugins.scan_packages}")
     private String[] scanPackages;
 
@@ -81,17 +81,32 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
     private void registerRuleNodeComponents() {
         Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
         for (BeanDefinition def : ruleNodeBeanDefinitions) {
-            try {
-                String clazzName = def.getBeanClassName();
-                Class<?> clazz = Class.forName(clazzName);
-                RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
-                ComponentType type = ruleNodeAnnotation.type();
-                ComponentDescriptor component = scanAndPersistComponent(def, type);
-                components.put(component.getClazz(), component);
-                componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
-            } catch (Exception e) {
-                log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
-                throw new RuntimeException(e);
+            int retryCount = 0;
+            Exception cause = null;
+            while (retryCount < MAX_OPTIMISITC_RETRIES) {
+                try {
+                    String clazzName = def.getBeanClassName();
+                    Class<?> clazz = Class.forName(clazzName);
+                    RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+                    ComponentType type = ruleNodeAnnotation.type();
+                    ComponentDescriptor component = scanAndPersistComponent(def, type);
+                    components.put(component.getClazz(), component);
+                    componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
+                    break;
+                } catch (Exception e) {
+                    log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+                    cause = e;
+                    retryCount++;
+                    try {
+                        Thread.sleep(1000);
+                    } catch (InterruptedException e1) {
+                        throw new RuntimeException(e1);
+                    }
+                }
+            }
+            if (cause != null && retryCount == MAX_OPTIMISITC_RETRIES) {
+                log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), cause.getMessage(), cause);
+                throw new RuntimeException(cause);
             }
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index 5526953..a2aeefa 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -251,7 +251,8 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
                 log.info("Entity views restored.");
 
                 break;
-
+            case "2.1.3":
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index 2c7f89c..b4a725d 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -149,7 +149,14 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
                     log.info("Entity views restored.");
                 }
                 break;
-
+            case "2.1.3":
+                try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+                    log.info("Updating schema ...");
+                    schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.2.0", SCHEMA_UPDATE_SQL);
+                    loadSql(schemaUpdateFile, conn);
+                    log.info("Schema updated.");
+                }
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
index 64f5213..a65c1a5 100644
--- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
@@ -90,7 +90,7 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
 
     @Override
     public void processRestAPIRpcRequestToRuleEngine(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer) {
-        log.trace("[{}] Processing local rpc call to rule engine [{}]", request.getTenantId(), request.getDeviceId());
+        log.trace("[{}][{}] Processing REST API call to rule engine [{}]", request.getTenantId(), request.getId(), request.getDeviceId());
         UUID requestId = request.getId();
         localToRuleEngineRpcRequests.put(requestId, responseConsumer);
         sendRpcRequestToRuleEngine(request);
@@ -98,31 +98,11 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
     }
 
     @Override
-    public void processRestAPIRpcResponseFromRuleEngine(FromDeviceRpcResponse response) {
-        UUID requestId = response.getId();
-        Consumer<FromDeviceRpcResponse> consumer = localToRuleEngineRpcRequests.remove(requestId);
-        if (consumer != null) {
-            consumer.accept(response);
-        } else {
-            log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
-        }
-    }
-
-    @Override
-    public void processRpcRequestToDevice(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer) {
-        log.trace("[{}] Processing local rpc call to device [{}]", request.getTenantId(), request.getDeviceId());
-        UUID requestId = request.getId();
-        localToDeviceRpcRequests.put(requestId, responseConsumer);
-        sendRpcRequestToDevice(request);
-        scheduleTimeout(request, requestId, localToDeviceRpcRequests);
-    }
-
-    @Override
-    public void processRpcResponseFromDevice(FromDeviceRpcResponse response) {
-        log.trace("[{}] response to request: [{}]", this.hashCode(), response.getId());
-        if (routingService.getCurrentServer().equals(response.getServerAddress())) {
+    public void processResponseToServerSideRPCRequestFromRuleEngine(ServerAddress requestOriginAddress, FromDeviceRpcResponse response) {
+        log.trace("[{}] Received response to server-side RPC request from rule engine: [{}]", response.getId(), requestOriginAddress);
+        if (routingService.getCurrentServer().equals(requestOriginAddress)) {
             UUID requestId = response.getId();
-            Consumer<FromDeviceRpcResponse> consumer = localToDeviceRpcRequests.remove(requestId);
+            Consumer<FromDeviceRpcResponse> consumer = localToRuleEngineRpcRequests.remove(requestId);
             if (consumer != null) {
                 consumer.accept(response);
             } else {
@@ -138,12 +118,33 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
             } else {
                 builder.setError(-1);
             }
-            rpcService.tell(response.getServerAddress(), ClusterAPIProtos.MessageType.CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE, builder.build().toByteArray());
+            rpcService.tell(requestOriginAddress, ClusterAPIProtos.MessageType.CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE, builder.build().toByteArray());
+        }
+    }
+
+    @Override
+    public void forwardServerSideRPCRequestToDeviceActor(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer) {
+        log.trace("[{}][{}] Processing local rpc call to device actor [{}]", request.getTenantId(), request.getId(), request.getDeviceId());
+        UUID requestId = request.getId();
+        localToDeviceRpcRequests.put(requestId, responseConsumer);
+        sendRpcRequestToDevice(request);
+        scheduleTimeout(request, requestId, localToDeviceRpcRequests);
+    }
+
+    @Override
+    public void processResponseToServerSideRPCRequestFromDeviceActor(FromDeviceRpcResponse response) {
+        log.trace("[{}] Received response to server-side RPC request from device actor.", response.getId());
+        UUID requestId = response.getId();
+        Consumer<FromDeviceRpcResponse> consumer = localToDeviceRpcRequests.remove(requestId);
+        if (consumer != null) {
+            consumer.accept(response);
+        } else {
+            log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
         }
     }
 
     @Override
-    public void processRemoteResponseFromDevice(ServerAddress serverAddress, byte[] data) {
+    public void processResponseToServerSideRPCRequestFromRemoteServer(ServerAddress serverAddress, byte[] data) {
         ClusterAPIProtos.FromDeviceRPCResponseProto proto;
         try {
             proto = ClusterAPIProtos.FromDeviceRPCResponseProto.parseFrom(data);
@@ -151,13 +152,12 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
             throw new RuntimeException(e);
         }
         RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null;
-        FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), routingService.getCurrentServer(),
-                proto.getResponse(), error);
-        processRpcResponseFromDevice(response);
+        FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), proto.getResponse(), error);
+        processResponseToServerSideRPCRequestFromRuleEngine(routingService.getCurrentServer(), response);
     }
 
     @Override
-    public void sendRpcReplyToDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body) {
+    public void sendReplyToRpcCallFromDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body) {
         ToServerRpcResponseActorMsg rpcMsg = new ToServerRpcResponseActorMsg(tenantId, deviceId, new ToServerRpcResponseMsg(requestId, body));
         forward(deviceId, rpcMsg);
     }
@@ -166,6 +166,8 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
         ObjectNode entityNode = json.createObjectNode();
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("requestUUID", msg.getId().toString());
+        metaData.putValue("originHost", routingService.getCurrentServer().getHost());
+        metaData.putValue("originPort", Integer.toString(routingService.getCurrentServer().getPort()));
         metaData.putValue("expirationTime", Long.toString(msg.getExpirationTime()));
         metaData.putValue("oneway", Boolean.toString(msg.isOneway()));
 
@@ -176,7 +178,7 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
             TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), metaData, TbMsgDataType.JSON
                     , json.writeValueAsString(entityNode)
                     , null, null, 0L);
-            actorService.onMsg(new ServiceToRuleEngineMsg(msg.getTenantId(), tbMsg));
+            actorService.onMsg(new SendToClusterMsg(msg.getDeviceId(), new ServiceToRuleEngineMsg(msg.getTenantId(), tbMsg)));
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);
         }
@@ -199,7 +201,7 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
             log.trace("[{}] timeout the request: [{}]", this.hashCode(), requestId);
             Consumer<FromDeviceRpcResponse> consumer = requestsMap.remove(requestId);
             if (consumer != null) {
-                consumer.accept(new FromDeviceRpcResponse(requestId, null, null, RpcError.TIMEOUT));
+                consumer.accept(new FromDeviceRpcResponse(requestId, null, RpcError.TIMEOUT));
             }
         }, timeout, TimeUnit.MILLISECONDS);
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DeviceRpcService.java
index 4cee96e..bf12ec6 100644
--- a/application/src/main/java/org/thingsboard/server/service/rpc/DeviceRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DeviceRpcService.java
@@ -29,13 +29,13 @@ public interface DeviceRpcService {
 
     void processRestAPIRpcRequestToRuleEngine(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer);
 
-    void processRestAPIRpcResponseFromRuleEngine(FromDeviceRpcResponse response);
+    void processResponseToServerSideRPCRequestFromRuleEngine(ServerAddress requestOriginAddress, FromDeviceRpcResponse response);
 
-    void processRpcRequestToDevice(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer);
+    void forwardServerSideRPCRequestToDeviceActor(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer);
 
-    void processRpcResponseFromDevice(FromDeviceRpcResponse response);
+    void processResponseToServerSideRPCRequestFromDeviceActor(FromDeviceRpcResponse response);
 
-    void sendRpcReplyToDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body);
+    void processResponseToServerSideRPCRequestFromRemoteServer(ServerAddress serverAddress, byte[] data);
 
-    void processRemoteResponseFromDevice(ServerAddress serverAddress, byte[] bytes);
+    void sendReplyToRpcCallFromDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body);
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java
index 9c3ce9a..75506df 100644
--- a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java
@@ -32,8 +32,6 @@ import java.util.UUID;
 public class FromDeviceRpcResponse {
     @Getter
     private final UUID id;
-    @Getter
-    private final ServerAddress serverAddress;
     private final String response;
     private final RpcError error;
 
diff --git a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
new file mode 100644
index 0000000..6201dab
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE;
+
+/**
+ * Created by ashvayka on 29.10.18.
+ */
+@Service
+@Slf4j
+public class DefaultDeviceSessionCacheService implements DeviceSessionCacheService {
+
+    @Override
+    @Cacheable(cacheNames = SESSIONS_CACHE, key = "#deviceId")
+    public DeviceSessionsCacheEntry get(DeviceId deviceId) {
+        log.debug("[{}] Fetching session data from cache", deviceId);
+        return DeviceSessionsCacheEntry.newBuilder().addAllSessions(Collections.emptyList()).build();
+    }
+
+    @Override
+    @CachePut(cacheNames = SESSIONS_CACHE, key = "#deviceId")
+    public DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions) {
+        log.debug("[{}] Pushing session data from cache: {}", deviceId, sessions);
+        return sessions;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java
new file mode 100644
index 0000000..0a2e6a5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.session;
+
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
+
+/**
+ * Created by ashvayka on 29.10.18.
+ */
+public interface DeviceSessionCacheService {
+
+    DeviceSessionsCacheEntry get(DeviceId deviceId);
+
+    DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index f4e37db..610cd3b 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.attributes.AttributesService;
@@ -457,7 +458,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
             TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, stateData.getDeviceId(), stateData.getMetaData().copy(), TbMsgDataType.JSON
                     , json.writeValueAsString(state)
                     , null, null, 0L);
-            actorService.onMsg(new ServiceToRuleEngineMsg(stateData.getTenantId(), tbMsg));
+            actorService.onMsg(new SendToClusterMsg(stateData.getDeviceId(), new ServiceToRuleEngineMsg(stateData.getTenantId(), tbMsg)));
         } catch (Exception e) {
             log.warn("[{}] Failed to push inactivity alarm: {}", stateData.getDeviceId(), state, e);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 3b0deea..7c77242 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -166,7 +166,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
 
     private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) {
         Map<String, Long> keyStates;
-        if(sub.isAllKeys()) {
+        if (sub.isAllKeys()) {
             keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L));
         } else {
             keyStates = sub.getKeyStates().entrySet()
@@ -618,7 +618,9 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
         builder.setEntityId(sub.getEntityId().getId().toString());
         builder.setType(sub.getType().name());
         builder.setAllKeys(sub.isAllKeys());
-        builder.setScope(sub.getScope());
+        if (sub.getScope() != null) {
+            builder.setScope(sub.getScope());
+        }
         sub.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(
                 ClusterAPIProtos.SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
         rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE, builder.build().toByteArray());
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java
index 0f72230..3d12938 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java
@@ -133,59 +133,48 @@ public class LocalTransportService extends AbstractTransportService implements R
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSessionEvent(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSessionEvent(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setPostTelemetry(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setPostTelemetry(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setPostAttributes(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setPostAttributes(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setGetAttributes(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setGetAttributes(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToAttributes(msg).build(), callback);
-        }
+    public void process(SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscriptionInfo(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToRPC(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToAttributes(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setToDeviceRPCCallResponse(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToRPC(msg).build(), callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setToServerRPCCallRequest(msg).build(), callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setToDeviceRPCCallResponse(msg).build(), callback);
+    }
+
+    @Override
+    protected void doProcess(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
+        forwardToDeviceActor(TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setToServerRPCCallRequest(msg).build(), callback);
     }
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
index 38db1a4..6c903cc 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
@@ -17,6 +17,11 @@ package org.thingsboard.server.service.transport;
 
 import akka.actor.ActorRef;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.BlockingBucket;
+import io.github.bucket4j.Bucket4j;
+import io.github.bucket4j.local.LocalBucket;
+import io.github.bucket4j.local.LocalBucketBuilder;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
 import org.apache.kafka.clients.producer.Callback;
@@ -49,6 +54,7 @@ import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 /**
@@ -68,6 +74,13 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
     @Value("${transport.remote.rule_engine.auto_commit_interval}")
     private int autoCommitInterval;
 
+    @Value("${transport.remote.rule_engine.poll_records_pack_size}")
+    private int pollRecordsPackSize;
+    @Value("${transport.remote.rule_engine.max_poll_records_per_second}")
+    private long pollRecordsPerSecond;
+    @Value("${transport.remote.rule_engine.max_poll_records_per_minute}")
+    private long pollRecordsPerMinute;
+
     @Autowired
     private TbKafkaSettings kafkaSettings;
 
@@ -109,15 +122,30 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
         ruleEngineConsumerBuilder.groupId("tb-node");
         ruleEngineConsumerBuilder.autoCommit(true);
         ruleEngineConsumerBuilder.autoCommitIntervalMs(autoCommitInterval);
+        ruleEngineConsumerBuilder.maxPollRecords(pollRecordsPackSize);
         ruleEngineConsumerBuilder.decoder(new ToRuleEngineMsgDecoder());
 
         ruleEngineConsumer = ruleEngineConsumerBuilder.build();
         ruleEngineConsumer.subscribe();
 
+        LocalBucketBuilder builder = Bucket4j.builder();
+        builder.addLimit(Bandwidth.simple(pollRecordsPerSecond, Duration.ofSeconds(1)));
+        builder.addLimit(Bandwidth.simple(pollRecordsPerMinute, Duration.ofMinutes(1)));
+        LocalBucket pollRateBucket = builder.build();
+        BlockingBucket blockingPollRateBucket = pollRateBucket.asScheduler();
+
         mainConsumerExecutor.execute(() -> {
             while (!stopped) {
                 try {
                     ConsumerRecords<String, byte[]> records = ruleEngineConsumer.poll(Duration.ofMillis(pollDuration));
+                    int recordsCount = records.count();
+                    if (recordsCount > 0) {
+                        while (!blockingPollRateBucket.tryConsume(recordsCount, TimeUnit.SECONDS.toNanos(5))) {
+                            log.info("Rule Engine consumer is busy. Required tokens: [{}]. Available tokens: [{}].", recordsCount, pollRateBucket.getAvailableTokens());
+                            Thread.sleep(TimeUnit.SECONDS.toMillis(1));
+                        }
+                        log.trace("Processing {} records", recordsCount);
+                    }
                     records.forEach(record -> {
                         try {
                             ToRuleEngineMsg toRuleEngineMsg = ruleEngineConsumer.decode(record);
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
index 21c963b..1940b36 100644
--- a/application/src/main/proto/cluster.proto
+++ b/application/src/main/proto/cluster.proto
@@ -22,6 +22,7 @@ option java_outer_classname = "ClusterAPIProtos";
 service ClusterRpcService {
   rpc handleMsgs(stream ClusterMessage) returns (stream ClusterMessage) {}
 }
+
 message ClusterMessage {
   MessageType messageType = 1;
   MessageMataInfo messageMetaInfo = 2;
@@ -139,4 +140,4 @@ message DeviceStateServiceMsgProto {
     bool added = 5;
     bool updated = 6;
     bool deleted = 7;
-}
\ No newline at end of file
+}
diff --git a/application/src/main/resources/actor-system.conf b/application/src/main/resources/actor-system.conf
index 3c68775..763319e 100644
--- a/application/src/main/resources/actor-system.conf
+++ b/application/src/main/resources/actor-system.conf
@@ -92,7 +92,7 @@ core-dispatcher {
   throughput = 5
 }
 
-# This dispatcher is used for system rule actors
+# This dispatcher is used for system rule chains and rule node actors
 system-rule-dispatcher {
   type = Dispatcher
   executor = "fork-join-executor"
@@ -115,30 +115,7 @@ system-rule-dispatcher {
   throughput = 5
 }
 
-# This dispatcher is used for system plugin actors
-system-plugin-dispatcher {
-  type = Dispatcher
-  executor = "fork-join-executor"
-  fork-join-executor {
-    # Min number of threads to cap factor-based parallelism number to
-    parallelism-min = 2
-    # Max number of threads to cap factor-based parallelism number to
-    parallelism-max = 12
-
-    # The parallelism factor is used to determine thread pool size using the
-    # following formula: ceil(available processors * factor). Resulting size
-    # is then bounded by the parallelism-min and parallelism-max values.
-    parallelism-factor = 0.25
-  }
-  # How long time the dispatcher will wait for new actors until it shuts down
-  shutdown-timeout = 1s
-
-  # Throughput defines the number of messages that are processed in a batch
-  # before the thread is returned to the pool. Set to 1 for as fair as possible.
-  throughput = 5
-}
-
-# This dispatcher is used for tenant rule actors
+# This dispatcher is used for tenant rule chains and rule node actors
 rule-dispatcher {
   type = Dispatcher
   executor = "fork-join-executor"
@@ -160,50 +137,3 @@ rule-dispatcher {
   # before the thread is returned to the pool. Set to 1 for as fair as possible.
   throughput = 5
 }
-
-# This dispatcher is used for tenant plugin actors
-plugin-dispatcher {
-  type = Dispatcher
-  executor = "fork-join-executor"
-  fork-join-executor {
-      # Min number of threads to cap factor-based parallelism number to
-      parallelism-min = 2
-      # Max number of threads to cap factor-based parallelism number to
-      parallelism-max = 12
-
-      # The parallelism factor is used to determine thread pool size using the
-      # following formula: ceil(available processors * factor). Resulting size
-      # is then bounded by the parallelism-min and parallelism-max values.
-      parallelism-factor = 0.25
-  }
-  # How long time the dispatcher will wait for new actors until it shuts down
-  shutdown-timeout = 1s
-
-  # Throughput defines the number of messages that are processed in a batch
-  # before the thread is returned to the pool. Set to 1 for as fair as possible.
-  throughput = 5
-}
-
-
-# This dispatcher is used for rule actors
-session-dispatcher {
-  type = Dispatcher
-  executor = "fork-join-executor"
-  fork-join-executor {
-      # Min number of threads to cap factor-based parallelism number to
-      parallelism-min = 2
-      # Max number of threads to cap factor-based parallelism number to
-      parallelism-max = 12
-
-      # The parallelism factor is used to determine thread pool size using the
-      # following formula: ceil(available processors * factor). Resulting size
-      # is then bounded by the parallelism-min and parallelism-max values.
-      parallelism-factor = 0.25
-  }
-  # How long time the dispatcher will wait for new actors until it shuts down
-  shutdown-timeout = 1s
-
-  # Throughput defines the number of messages that are processed in a batch
-  # before the thread is returned to the pool. Set to 1 for as fair as possible.
-  throughput = 5
-}
\ No newline at end of file
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index da6db70..51ddaa9 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -140,7 +140,7 @@ cassandra:
     buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
     concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
     permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
-    rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
+    rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
 
 # SQL configuration parameters
 sql:
@@ -202,6 +202,9 @@ caffeine:
     devices:
       timeToLiveInMinutes: 1440
       maxSize: 100000
+    sessions:
+      timeToLiveInMinutes: 1440
+      maxSize: 100000
     assets:
       timeToLiveInMinutes: 1440
       maxSize: 100000
@@ -222,7 +225,7 @@ redis:
 updates:
   # Enable/disable updates checking.
   enabled: "${UPDATES_ENABLED:true}"
-  
+
 # spring CORS configuration
 spring.mvc.cors:
    mappings:
@@ -322,8 +325,8 @@ audit_log:
     password: "${AUDIT_LOG_SINK_PASSWORD:}"
 
 state:
-  defaultInactivityTimeoutInSec: 10
-  defaultStateCheckIntervalInSec: 10
+  defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:10}"
+  defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:10}"
 
 kafka:
   enabled: true
@@ -390,8 +393,14 @@ transport:
       topic: "${TB_RULE_ENGINE_TOPIC:tb.rule-engine}"
       poll_interval: "${TB_RULE_ENGINE_POLL_INTERVAL_MS:25}"
       auto_commit_interval: "${TB_RULE_ENGINE_AUTO_COMMIT_INTERVAL_MS:100}"
+      poll_records_pack_size: "${TB_RULE_ENGINE_MAX_POLL_RECORDS:1000}"
+      max_poll_records_per_second: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_SECOND:10000}"
+      max_poll_records_per_minute: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_MINUTE:120000}"
     notifications:
       topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}"
+  sessions:
+    inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
+    report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
   rate_limits:
     enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
     tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
index f050f3b..1de2787 100644
--- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
 import org.thingsboard.server.dao.attributes.AttributesService;
@@ -155,7 +156,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
                 device.getId(),
                 new TbMsgMetaData(),
                 "{}", null, null, 0L);
-        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+        actorService.onMsg(new SendToClusterMsg(device.getId(), new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg)));
 
         Thread.sleep(3000);
 
@@ -270,7 +271,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
                 device.getId(),
                 new TbMsgMetaData(),
                 "{}", null, null, 0L);
-        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+        actorService.onMsg(new SendToClusterMsg(device.getId(), new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg)));
 
         Thread.sleep(3000);
 
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
index 24db457..f59dd63 100644
--- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
 import org.thingsboard.server.dao.attributes.AttributesService;
@@ -142,7 +143,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
                 new TbMsgMetaData(),
                 "{}",
                 null, null, 0L);
-        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+        actorService.onMsg(new SendToClusterMsg(device.getId(), new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg)));
 
         Thread.sleep(3000);
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
index c37d460..822387c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
@@ -24,6 +24,7 @@ public enum ActionType {
     UPDATED(false), // log entity
     ATTRIBUTES_UPDATED(false), // log attributes/values
     ATTRIBUTES_DELETED(false), // log attributes
+    TIMESERIES_DELETED(false), // log timeseries
     RPC_CALL(false), // log method and params
     CREDENTIALS_UPDATED(false), // log new credentials
     ASSIGNED_TO_CUSTOMER(false), // log customer name
@@ -32,11 +33,11 @@ public enum ActionType {
     SUSPENDED(false), // log string id
     CREDENTIALS_READ(true), // log device id
     ATTRIBUTES_READ(true), // log attributes
-    RELATION_ADD_OR_UPDATE (false),
-    RELATION_DELETED (false),
-    RELATIONS_DELETED (false),
-    ALARM_ACK (false),
-    ALARM_CLEAR (false);
+    RELATION_ADD_OR_UPDATE(false),
+    RELATION_DELETED(false),
+    RELATIONS_DELETED(false),
+    ALARM_ACK(false),
+    ALARM_CLEAR(false);
 
     private final boolean isRead;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
index 698a69e..853caff 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
@@ -19,6 +19,7 @@ public class CacheConstants {
     public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
     public static final String RELATIONS_CACHE = "relations";
     public static final String DEVICE_CACHE = "devices";
+    public static final String SESSIONS_CACHE = "sessions";
     public static final String ASSET_CACHE = "assets";
     public static final String ENTITY_VIEW_CACHE = "entityViews";
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerType.java
index f9b057c..08f74fb 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerType.java
@@ -19,6 +19,5 @@ package org.thingsboard.server.common.msg.cluster;
  * Created by ashvayka on 23.09.18.
  */
 public enum ServerType {
-    //Should match content of enum in discovery.proto.
-    CORE, JS_EVALUATOR
+    CORE
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
index 24758b5..44a98d6 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -96,13 +96,8 @@ public enum MsgType {
      */
     DEVICE_ACTOR_TO_RULE_ENGINE_MSG,
 
-    /**
-     * Message that is sent from Rule Engine to the Device Actor when message is successfully pushed to queue.
-     */
-    ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG,
-    TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG,
     SESSION_TIMEOUT_MSG,
-    SESSION_CTRL_MSG,
+
     STATS_PERSIST_TICK_MSG,
 
 
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
index 0792b63..7859623 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
@@ -21,11 +21,13 @@ import org.thingsboard.server.common.msg.MsgType;
 import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.TbMsg;
 
+import java.io.Serializable;
+
 /**
  * Created by ashvayka on 15.03.18.
  */
 @Data
-public final class ServiceToRuleEngineMsg implements TbActorMsg {
+public final class ServiceToRuleEngineMsg implements TbActorMsg, Serializable {
 
     private final TenantId tenantId;
     private final TbMsg tbMsg;
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaConsumerTemplate.java
index 86be3a3..c8a1706 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaConsumerTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaConsumerTemplate.java
@@ -46,7 +46,8 @@ public class TBKafkaConsumerTemplate<T> {
     private TBKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder<T> decoder,
                                     TbKafkaRequestIdExtractor<T> requestIdExtractor,
                                     String clientId, String groupId, String topic,
-                                    boolean autoCommit, int autoCommitIntervalMs) {
+                                    boolean autoCommit, int autoCommitIntervalMs,
+                                    int maxPollRecords) {
         Properties props = settings.toProps();
         props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId);
         if (groupId != null) {
@@ -56,6 +57,9 @@ public class TBKafkaConsumerTemplate<T> {
         props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs);
         props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
         props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
+        if (maxPollRecords > 0) {
+            props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
+        }
         this.consumer = new KafkaConsumer<>(props);
         this.decoder = decoder;
         this.requestIdExtractor = requestIdExtractor;
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
index 610a490..bd42f31 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
@@ -26,6 +26,7 @@ import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.clients.producer.RecordMetadata;
 import org.apache.kafka.common.PartitionInfo;
+import org.apache.kafka.common.errors.TopicExistsException;
 import org.apache.kafka.common.header.Header;
 
 import java.util.List;
@@ -75,7 +76,11 @@ public class TBKafkaProducerTemplate<T> {
             CreateTopicsResult result = admin.createTopic(new NewTopic(defaultTopic, 100, (short) 1));
             result.all().get();
         } catch (Exception e) {
-            log.trace("Failed to create topic: {}", e.getMessage(), e);
+            if ((e instanceof TopicExistsException) || (e.getCause() != null && e.getCause() instanceof TopicExistsException)) {
+                log.trace("[{}] Topic already exists: ", defaultTopic);
+            } else {
+                log.trace("[{}] Failed to create topic: {}", defaultTopic, e.getMessage(), e);
+            }
         }
         //Maybe this should not be cached, but we don't plan to change size of partitions
         this.partitionInfoMap = new ConcurrentHashMap<>();
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index da8b3a6..a178bdf 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -43,10 +43,10 @@ import org.thingsboard.server.common.transport.TransportService;
 import org.thingsboard.server.common.transport.TransportServiceCallback;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
 import org.thingsboard.server.common.msg.EncryptionUtil;
+import org.thingsboard.server.common.transport.service.AbstractTransportService;
 import org.thingsboard.server.gen.transport.TransportProtos;
 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
 import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent;
-import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg;
 import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
@@ -141,9 +141,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
                 processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg);
                 break;
             case PINGREQ:
-                //TODO: should we push the notification to the rule engine?
                 if (checkConnected(ctx)) {
                     ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
+                    transportService.reportActivity(sessionInfo);
+                    if (gatewaySessionHandler != null) {
+                        gatewaySessionHandler.reportActivity();
+                    }
                 }
                 break;
             case DISCONNECT:
@@ -394,7 +397,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
     private void processDisconnect(ChannelHandlerContext ctx) {
         ctx.close();
         if (deviceSessionCtx.isConnected()) {
-            transportService.process(sessionInfo, getSessionEventMsg(SessionEvent.CLOSED), null);
+            transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.CLOSED), null);
             transportService.deregisterSession(sessionInfo);
             if (gatewaySessionHandler != null) {
                 gatewaySessionHandler.onGatewayDisconnect();
@@ -466,16 +469,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         }
     }
 
-    public static SessionEventMsg getSessionEventMsg(SessionEvent event) {
-        return SessionEventMsg.newBuilder()
-                .setSessionType(TransportProtos.SessionType.ASYNC)
-                .setEvent(event).build();
-    }
-
     @Override
     public void operationComplete(Future<? super Void> future) throws Exception {
         if (deviceSessionCtx.isConnected()) {
-            transportService.process(sessionInfo, getSessionEventMsg(SessionEvent.CLOSED), null);
+            transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.CLOSED), null);
             transportService.deregisterSession(sessionInfo);
         }
     }
@@ -495,7 +492,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
                     .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB())
                     .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB())
                     .build();
-            transportService.process(sessionInfo, getSessionEventMsg(SessionEvent.OPEN), null);
+            transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.OPEN), null);
             transportService.registerAsyncSession(sessionInfo, this);
             checkGatewaySession();
             ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
index d600059..ac33ba6 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
@@ -34,6 +34,7 @@ import org.thingsboard.server.common.transport.TransportService;
 import org.thingsboard.server.common.transport.TransportServiceCallback;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.common.transport.service.AbstractTransportService;
 import org.thingsboard.server.gen.transport.TransportProtos;
 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
@@ -118,7 +119,7 @@ public class GatewaySessionHandler {
                             GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), mqttQoSMap);
                             if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) {
                                 SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo();
-                                transportService.process(deviceSessionInfo, MqttTransportHandler.getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null);
+                                transportService.process(deviceSessionInfo, AbstractTransportService.getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null);
                                 transportService.process(deviceSessionInfo, TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), null);
                                 transportService.process(deviceSessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), null);
                                 transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx);
@@ -334,7 +335,7 @@ public class GatewaySessionHandler {
 
     private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) {
         transportService.deregisterSession(deviceSessionCtx.getSessionInfo());
-        transportService.process(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
+        transportService.process(deviceSessionCtx.getSessionInfo(), AbstractTransportService.getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
         log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName);
     }
 
@@ -360,11 +361,15 @@ public class GatewaySessionHandler {
         return context;
     }
 
-    public MqttTransportAdaptor getAdaptor() {
+    MqttTransportAdaptor getAdaptor() {
         return context.getAdaptor();
     }
 
-    public int nextMsgId() {
+    int nextMsgId() {
         return deviceSessionCtx.nextMsgId();
     }
+
+    public void reportActivity() {
+        devices.forEach((id, deviceCtx) -> transportService.reportActivity(deviceCtx.getSessionInfo()));
+    }
 }
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
index 265dacb..fad1954 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
@@ -47,9 +47,14 @@ public abstract class AbstractTransportService implements TransportService {
     private String perTenantLimitsConf;
     @Value("${transport.rate_limits.tenant}")
     private String perDevicesLimitsConf;
+    @Value("${transport.sessions.inactivity_timeout}")
+    private long sessionInactivityTimeout;
+    @Value("${transport.sessions.report_timeout}")
+    private long sessionReportTimeout;
 
     protected ScheduledExecutorService schedulerExecutor;
     protected ExecutorService transportCallbackExecutor;
+
     private ConcurrentMap<UUID, SessionMetaData> sessions = new ConcurrentHashMap<>();
 
     //TODO: Implement cleanup of this maps.
@@ -59,7 +64,121 @@ public abstract class AbstractTransportService implements TransportService {
     @Override
     public void registerAsyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener) {
         sessions.putIfAbsent(toId(sessionInfo), new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener));
-        //TODO: monitor sessions periodically: PING REQ/RESP, etc.
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            SessionMetaData sessionMetaData = reportActivityInternal(sessionInfo);
+            sessionMetaData.setSubscribedToAttributes(!msg.getUnsubscribe());
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            SessionMetaData sessionMetaData = reportActivityInternal(sessionInfo);
+            sessionMetaData.setSubscribedToRPC(!msg.getUnsubscribe());
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
+        if (checkLimits(sessionInfo, callback)) {
+            reportActivityInternal(sessionInfo);
+            doProcess(sessionInfo, msg, callback);
+        }
+    }
+
+    @Override
+    public void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
+        reportActivityInternal(sessionInfo);
+    }
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback);
+
+    protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback);
+
+    private SessionMetaData reportActivityInternal(TransportProtos.SessionInfoProto sessionInfo) {
+        UUID sessionId = toId(sessionInfo);
+        SessionMetaData sessionMetaData = sessions.get(sessionId);
+        if (sessionMetaData != null) {
+            sessionMetaData.updateLastActivityTime();
+        }
+        return sessionMetaData;
+    }
+
+    private void checkInactivityAndReportActivity() {
+        long expTime = System.currentTimeMillis() - sessionInactivityTimeout;
+        sessions.forEach((uuid, sessionMD) -> {
+            if (sessionMD.getLastActivityTime() < expTime) {
+                if (log.isDebugEnabled()) {
+                    log.debug("[{}] Session has expired due to last activity time: {}", toId(sessionMD.getSessionInfo()), sessionMD.getLastActivityTime());
+                }
+                process(sessionMD.getSessionInfo(), getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
+                sessions.remove(uuid);
+                sessionMD.getListener().onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto.getDefaultInstance());
+            } else {
+                process(sessionMD.getSessionInfo(), TransportProtos.SubscriptionInfoProto.newBuilder()
+                        .setAttributeSubscription(sessionMD.isSubscribedToAttributes())
+                        .setRpcSubscription(sessionMD.isSubscribedToRPC())
+                        .setLastActivityTime(sessionMD.getLastActivityTime()).build(), null);
+            }
+        });
     }
 
     @Override
@@ -131,7 +250,7 @@ public abstract class AbstractTransportService implements TransportService {
         }
     }
 
-    protected UUID toId(TransportProtos.SessionInfoProto sessionInfo) {
+    private UUID toId(TransportProtos.SessionInfoProto sessionInfo) {
         return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
     }
 
@@ -147,6 +266,7 @@ public abstract class AbstractTransportService implements TransportService {
         }
         this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor();
         this.transportCallbackExecutor = new ThreadPoolExecutor(0, 20, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
+        this.schedulerExecutor.scheduleAtFixedRate(this::checkInactivityAndReportActivity, sessionReportTimeout, sessionReportTimeout, TimeUnit.MILLISECONDS);
     }
 
     public void destroy() {
@@ -161,4 +281,10 @@ public abstract class AbstractTransportService implements TransportService {
             transportCallbackExecutor.shutdownNow();
         }
     }
+
+    public static TransportProtos.SessionEventMsg getSessionEventMsg(TransportProtos.SessionEvent event) {
+        return TransportProtos.SessionEventMsg.newBuilder()
+                .setSessionType(TransportProtos.SessionType.ASYNC)
+                .setEvent(event).build();
+    }
 }
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
index 6774942..4b11bf5 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
@@ -217,91 +217,84 @@ public class RemoteTransportService extends AbstractTransportService {
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setSessionEvent(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    public void process(SessionInfoProto sessionInfo, SubscriptionInfoProto msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setSubscriptionInfo(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setPostTelemetry(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setSessionEvent(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setPostAttributes(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setPostTelemetry(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setGetAttributes(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setPostAttributes(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setSubscribeToAttributes(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, GetAttributeRequestMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setGetAttributes(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setSubscribeToRPC(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setSubscribeToAttributes(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setToDeviceRPCCallResponse(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, SubscribeToRPCMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setSubscribeToRPC(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     @Override
-    public void process(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
-        if (checkLimits(sessionInfo, callback)) {
-            ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
-                    TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
-                            .setToServerRPCCallRequest(msg).build()
-            ).build();
-            send(sessionInfo, toRuleEngineMsg, callback);
-        }
+    protected void doProcess(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setToDeviceRPCCallResponse(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
+    }
+
+    @Override
+    protected void doProcess(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
+        ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg(
+                TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
+                        .setToServerRPCCallRequest(msg).build()
+        ).build();
+        send(sessionInfo, toRuleEngineMsg, callback);
     }
 
     private static class TransportCallbackAdaptor implements Callback {
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java
index 1de5711..8642e93 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java
@@ -23,10 +23,25 @@ import org.thingsboard.server.gen.transport.TransportProtos;
  * Created by ashvayka on 15.10.18.
  */
 @Data
-public class SessionMetaData {
+class SessionMetaData {
 
     private final TransportProtos.SessionInfoProto sessionInfo;
     private final TransportProtos.SessionType sessionType;
     private final SessionMsgListener listener;
 
+    private volatile long lastActivityTime;
+    private volatile boolean subscribedToAttributes;
+    private volatile boolean subscribedToRPC;
+
+    SessionMetaData(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionType sessionType, SessionMsgListener listener) {
+        this.sessionInfo = sessionInfo;
+        this.sessionType = sessionType;
+        this.listener = listener;
+        this.lastActivityTime = System.currentTimeMillis();
+    }
+
+    void updateLastActivityTime() {
+        this.lastActivityTime = System.currentTimeMillis();
+    }
+
 }
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
index a47438f..8944e94 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
@@ -61,10 +61,14 @@ public interface TransportService {
 
     void process(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback);
 
+    void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback<Void> callback);
+
     void registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener);
 
     void registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout);
 
+    void reportActivity(SessionInfoProto sessionInfo);
+
     void deregisterSession(SessionInfoProto sessionInfo);
 
 }
diff --git a/common/transport/transport-api/src/main/proto/transport.proto b/common/transport/transport-api/src/main/proto/transport.proto
index 0e36dab..ff740d4 100644
--- a/common/transport/transport-api/src/main/proto/transport.proto
+++ b/common/transport/transport-api/src/main/proto/transport.proto
@@ -172,6 +172,22 @@ message ToServerRpcResponseMsg {
   string error = 3;
 }
 
+//Used to report session state to tb-node and persist this state in the cache on the tb-node level.
+message SubscriptionInfoProto {
+  int64 lastActivityTime = 1;
+  bool attributeSubscription = 2;
+  bool rpcSubscription = 3;
+}
+
+message SessionSubscriptionInfoProto {
+  SessionInfoProto sessionInfo = 1;
+  SubscriptionInfoProto subscriptionInfo = 2;
+}
+
+message DeviceSessionsCacheEntry {
+  repeated SessionSubscriptionInfoProto sessions = 1;
+}
+
 message TransportToDeviceActorMsg {
   SessionInfoProto sessionInfo = 1;
   SessionEventMsg sessionEvent = 2;
@@ -182,6 +198,7 @@ message TransportToDeviceActorMsg {
   SubscribeToRPCMsg subscribeToRPC = 7;
   ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 8;
   ToServerRpcRequestMsg toServerRPCCallRequest = 9;
+  SubscriptionInfoProto subscriptionInfo = 10;
 }
 
 message DeviceActorToTransportMsg {
@@ -214,4 +231,4 @@ message TransportApiRequestMsg {
 message TransportApiResponseMsg {
    ValidateDeviceCredentialsResponseMsg validateTokenResponseMsg = 1;
    GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2;
-}
\ No newline at end of file
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
index da87b44..9c82866 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
@@ -43,6 +43,8 @@ public interface EntityViewService {
 
     EntityView findEntityViewById(EntityViewId entityViewId);
 
+    EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name);
+
     TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink);
 
     TextPageData<EntityView> findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
index 2d94cc2..9f8949d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
@@ -29,8 +29,6 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.cache.annotation.Caching;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.DataConstants;
-import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.EntityView;
@@ -40,12 +38,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityViewId;
 import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.dao.attributes.AttributesService;
 import org.thingsboard.server.dao.customer.CustomerDao;
 import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
@@ -56,15 +52,13 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE;
-import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 import static org.thingsboard.server.dao.service.Validator.validateId;
 import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@@ -96,6 +90,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
 
     @Caching(evict = {
             @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.entityId}"),
+            @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.name}"),
             @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.id}")})
     @Override
     public EntityView saveEntityView(EntityView entityView) {
@@ -137,6 +132,15 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
         return entityViewDao.findById(entityViewId.getId());
     }
 
+    @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#tenantId, #name}")
+    @Override
+    public EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name) {
+        log.trace("Executing findEntityViewByTenantIdAndName [{}][{}]", tenantId, name);
+        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+        Optional<EntityView> entityViewOpt = entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name);
+        return entityViewOpt.orElse(null);
+    }
+
     @Override
     public TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink) {
         log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@@ -255,6 +259,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
         deleteEntityRelations(entityViewId);
         EntityView entityView = entityViewDao.findById(entityViewId.getId());
         cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId()));
+        cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getName()));
         entityViewDao.removeById(entityViewId.getId());
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
index 66fbcc3..e4c6bb5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
@@ -34,6 +34,7 @@ import javax.persistence.Entity;
 import javax.persistence.EnumType;
 import javax.persistence.Enumerated;
 import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -53,7 +54,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
     @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY)
     private String name;
 
-    @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY)
+    @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, unique = true)
     private String clazz;
 
     @Type(type = "json")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index d1af167..b38110b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -35,7 +35,6 @@ import org.thingsboard.server.dao.model.type.ComponentTypeCodec;
 import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
 import org.thingsboard.server.dao.model.type.EntityTypeCodec;
 import org.thingsboard.server.dao.model.type.JsonCodec;
-import org.thingsboard.server.dao.util.BufferedRateLimiter;
 
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -49,7 +48,7 @@ public abstract class CassandraAbstractDao {
     private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
 
     @Autowired
-    private BufferedRateLimiter rateLimiter;
+    private CassandraBufferedRateExecutor rateLimiter;
 
     private Session session;
 
@@ -115,12 +114,12 @@ public abstract class CassandraAbstractDao {
         if (statement.getConsistencyLevel() == null) {
             statement.setConsistencyLevel(level);
         }
-        return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
+        return rateLimiter.submit(new CassandraStatementTask(getSession(), statement));
     }
 
     private static String statementToString(Statement statement) {
         if (statement instanceof BoundStatement) {
-            return ((BoundStatement)statement).preparedStatement().getQueryString();
+            return ((BoundStatement) statement).preparedStatement().getQueryString();
         } else {
             return statement.toString();
         }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
new file mode 100644
index 0000000..a3490bf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor;
+import org.thingsboard.server.dao.util.AsyncTaskContext;
+import org.thingsboard.server.dao.util.NoSqlAnyDao;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+@Component
+@Slf4j
+@NoSqlAnyDao
+public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor<CassandraStatementTask, ResultSetFuture, ResultSet> {
+
+    public CassandraBufferedRateExecutor(
+            @Value("${cassandra.query.buffer_size}") int queueLimit,
+            @Value("${cassandra.query.concurrent_limit}") int concurrencyLimit,
+            @Value("${cassandra.query.permit_max_wait_time}") long maxWaitTime,
+            @Value("${cassandra.query.dispatcher_threads:2}") int dispatcherThreads,
+            @Value("${cassandra.query.callback_threads:2}") int callbackThreads,
+            @Value("${cassandra.query.poll_ms:50}") long pollMs) {
+        super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs);
+    }
+
+    @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
+    public void printStats() {
+        log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] currBuffer [{}] ",
+                getQueueSize(),
+                totalAdded.getAndSet(0), totalLaunched.getAndSet(0), totalReleased.getAndSet(0),
+                totalFailed.getAndSet(0), totalExpired.getAndSet(0), totalRejected.getAndSet(0),
+                concurrencyLevel.get());
+    }
+
+    @PreDestroy
+    public void stop() {
+        super.stop();
+    }
+
+    @Override
+    protected SettableFuture<ResultSet> create() {
+        return SettableFuture.create();
+    }
+
+    @Override
+    protected ResultSetFuture wrap(CassandraStatementTask task, SettableFuture<ResultSet> future) {
+        return new TbResultSetFuture(future);
+    }
+
+    @Override
+    protected ResultSetFuture execute(AsyncTaskContext<CassandraStatementTask, ResultSet> taskCtx) {
+        CassandraStatementTask task = taskCtx.getTask();
+        return task.getSession().executeAsync(task.getStatement());
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java
new file mode 100644
index 0000000..19e5640
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
+import lombok.Data;
+import org.thingsboard.server.dao.util.AsyncTask;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+@Data
+public class CassandraStatementTask implements AsyncTask {
+
+    private final Session session;
+    private final Statement statement;
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java
new file mode 100644
index 0000000..574a5f5
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+public class TbResultSetFuture implements ResultSetFuture {
+
+    private final SettableFuture<ResultSet> mainFuture;
+
+    public TbResultSetFuture(SettableFuture<ResultSet> mainFuture) {
+        this.mainFuture = mainFuture;
+    }
+
+    @Override
+    public ResultSet getUninterruptibly() {
+        return getSafe();
+    }
+
+    @Override
+    public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException {
+        return getSafe(timeout, unit);
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return mainFuture.cancel(mayInterruptIfRunning);
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return mainFuture.isCancelled();
+    }
+
+    @Override
+    public boolean isDone() {
+        return mainFuture.isDone();
+    }
+
+    @Override
+    public ResultSet get() throws InterruptedException, ExecutionException {
+        return mainFuture.get();
+    }
+
+    @Override
+    public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        return mainFuture.get(timeout, unit);
+    }
+
+    @Override
+    public void addListener(Runnable listener, Executor executor) {
+        mainFuture.addListener(listener, executor);
+    }
+
+    private ResultSet getSafe() {
+        try {
+            return mainFuture.get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private ResultSet getSafe(long timeout, TimeUnit unit) throws TimeoutException {
+        try {
+            return mainFuture.get(timeout, unit);
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 5bd9175..04227a3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -31,6 +32,7 @@ import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.UUIDConverter;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
 import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
@@ -41,9 +43,9 @@ import org.thingsboard.server.dao.model.sql.TsKvEntity;
 import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
 import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
 import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
+import org.thingsboard.server.dao.timeseries.SimpleListenableFuture;
 import org.thingsboard.server.dao.timeseries.TimeseriesDao;
 import org.thingsboard.server.dao.timeseries.TsInsertExecutorType;
-import org.thingsboard.server.dao.util.SqlDao;
 import org.thingsboard.server.dao.util.SqlTsDao;
 
 import javax.annotation.Nullable;
@@ -53,6 +55,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
@@ -64,6 +67,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 @SqlTsDao
 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
 
+    private static final String DESC_ORDER = "DESC";
+
     @Value("${sql.ts_inserts_executor_type}")
     private String insertExecutorType;
 
@@ -326,14 +331,72 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
 
     @Override
     public ListenableFuture<Void> removeLatest(EntityId entityId, DeleteTsKvQuery query) {
-        TsKvLatestEntity latestEntity = new TsKvLatestEntity();
-        latestEntity.setEntityType(entityId.getEntityType());
-        latestEntity.setEntityId(fromTimeUUID(entityId.getId()));
-        latestEntity.setKey(query.getKey());
-        return service.submit(() -> {
-            tsKvLatestRepository.delete(latestEntity);
-            return null;
+        ListenableFuture<TsKvEntry> latestFuture = findLatest(entityId, query.getKey());
+
+        ListenableFuture<Boolean> booleanFuture = Futures.transform(latestFuture, tsKvEntry -> {
+            long ts = tsKvEntry.getTs();
+            return ts > query.getStartTs() && ts <= query.getEndTs();
+        }, service);
+
+        ListenableFuture<Void> removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
+            if (isRemove) {
+                TsKvLatestEntity latestEntity = new TsKvLatestEntity();
+                latestEntity.setEntityType(entityId.getEntityType());
+                latestEntity.setEntityId(fromTimeUUID(entityId.getId()));
+                latestEntity.setKey(query.getKey());
+                return service.submit(() -> {
+                    tsKvLatestRepository.delete(latestEntity);
+                    return null;
+                });
+            }
+            return Futures.immediateFuture(null);
+        }, service);
+
+        final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
+        Futures.addCallback(removedLatestFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                if (query.getRewriteLatestIfDeleted()) {
+                    ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
+                        if (isRemove) {
+                            return getNewLatestEntryFuture(entityId, query);
+                        }
+                        return Futures.immediateFuture(null);
+                    }, service);
+
+                    try {
+                        resultFuture.set(savedLatestFuture.get());
+                    } catch (InterruptedException | ExecutionException e) {
+                        log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e);
+                    }
+                } else {
+                    resultFuture.set(null);
+                }
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                log.warn("[{}] Failed to process remove of the latest value", entityId, t);
+            }
         });
+        return resultFuture;
+    }
+
+    private ListenableFuture<Void> getNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) {
+        long startTs = 0;
+        long endTs = query.getStartTs() - 1;
+        ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
+                Aggregation.NONE, DESC_ORDER);
+        ListenableFuture<List<TsKvEntry>> future = findAllAsync(entityId, findNewLatestQuery);
+
+        return Futures.transformAsync(future, entryList -> {
+            if (entryList.size() == 1) {
+                return saveLatest(entityId, entryList.get(0));
+            } else {
+                log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey());
+            }
+            return Futures.immediateFuture(null);
+        }, service);
     }
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
index 4c743e5..296d173 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
@@ -47,7 +47,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
     @Modifying
     @Query("DELETE FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " +
             "AND tskv.entityType = :entityType AND tskv.key = :entityKey " +
-            "AND tskv.ts > :startTs AND tskv.ts < :endTs")
+            "AND tskv.ts > :startTs AND tskv.ts <= :endTs")
     void delete(@Param("entityId") String entityId,
                 @Param("entityType") EntityType entityType,
                 @Param("entityKey") String key,
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index 709bfd5..fdc69f9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -48,7 +48,6 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao;
-import org.thingsboard.server.dao.util.NoSqlDao;
 import org.thingsboard.server.dao.util.NoSqlTsDao;
 
 import javax.annotation.Nullable;
@@ -62,6 +61,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
@@ -434,14 +434,14 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     public ListenableFuture<Void> removeLatest(EntityId entityId, DeleteTsKvQuery query) {
         ListenableFuture<TsKvEntry> latestEntryFuture = findLatest(entityId, query.getKey());
 
-        ListenableFuture<Boolean> booleanFuture = Futures.transformAsync(latestEntryFuture, latestEntry -> {
+        ListenableFuture<Boolean> booleanFuture = Futures.transform(latestEntryFuture, latestEntry -> {
             long ts = latestEntry.getTs();
-            if (ts >= query.getStartTs() && ts <= query.getEndTs()) {
-                return Futures.immediateFuture(true);
+            if (ts > query.getStartTs() && ts <= query.getEndTs()) {
+                return true;
             } else {
                 log.trace("Won't be deleted latest value for [{}], key - {}", entityId, query.getKey());
             }
-            return Futures.immediateFuture(false);
+            return false;
         }, readResultsProcessingExecutor);
 
         ListenableFuture<Void> removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
@@ -451,18 +451,34 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
             return Futures.immediateFuture(null);
         }, readResultsProcessingExecutor);
 
-        if (query.getRewriteLatestIfDeleted()) {
-            ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
-                if (isRemove) {
-                    return getNewLatestEntryFuture(entityId, query);
+        final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
+        Futures.addCallback(removedLatestFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                if (query.getRewriteLatestIfDeleted()) {
+                    ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
+                        if (isRemove) {
+                            return getNewLatestEntryFuture(entityId, query);
+                        }
+                        return Futures.immediateFuture(null);
+                    }, readResultsProcessingExecutor);
+
+                    try {
+                        resultFuture.set(savedLatestFuture.get());
+                    } catch (InterruptedException | ExecutionException e) {
+                        log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e);
+                    }
+                } else {
+                    resultFuture.set(null);
                 }
-                return Futures.immediateFuture(null);
-            }, readResultsProcessingExecutor);
+            }
 
-            return Futures.transformAsync(Futures.allAsList(Arrays.asList(savedLatestFuture, removedLatestFuture)),
-                    list -> Futures.immediateFuture(null), readResultsProcessingExecutor);
-        }
-        return removedLatestFuture;
+            @Override
+            public void onFailure(Throwable t) {
+                log.warn("[{}] Failed to process remove of the latest value", entityId, t);
+            }
+        });
+        return resultFuture;
     }
 
     private ListenableFuture<Void> getNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
new file mode 100644
index 0000000..96d3870
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
@@ -0,0 +1,175 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.Nullable;
+import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+@Slf4j
+public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extends ListenableFuture<V>, V> implements BufferedRateExecutor<T, F> {
+
+    private final long maxWaitTime;
+    private final long pollMs;
+    private final BlockingQueue<AsyncTaskContext<T, V>> queue;
+    private final ExecutorService dispatcherExecutor;
+    private final ExecutorService callbackExecutor;
+    private final ScheduledExecutorService timeoutExecutor;
+    private final int concurrencyLimit;
+
+    protected final AtomicInteger concurrencyLevel = new AtomicInteger();
+    protected final AtomicInteger totalAdded = new AtomicInteger();
+    protected final AtomicInteger totalLaunched = new AtomicInteger();
+    protected final AtomicInteger totalReleased = new AtomicInteger();
+    protected final AtomicInteger totalFailed = new AtomicInteger();
+    protected final AtomicInteger totalExpired = new AtomicInteger();
+    protected final AtomicInteger totalRejected = new AtomicInteger();
+
+    public AbstractBufferedRateExecutor(int queueLimit, int concurrencyLimit, long maxWaitTime, int dispatcherThreads, int callbackThreads, long pollMs) {
+        this.maxWaitTime = maxWaitTime;
+        this.pollMs = pollMs;
+        this.concurrencyLimit = concurrencyLimit;
+        this.queue = new LinkedBlockingDeque<>(queueLimit);
+        this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads);
+        this.callbackExecutor = Executors.newFixedThreadPool(callbackThreads);
+        this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
+        for (int i = 0; i < dispatcherThreads; i++) {
+            dispatcherExecutor.submit(this::dispatch);
+        }
+    }
+
+    @Override
+    public F submit(T task) {
+        SettableFuture<V> settableFuture = create();
+        F result = wrap(task, settableFuture);
+        try {
+            totalAdded.incrementAndGet();
+            queue.add(new AsyncTaskContext<>(UUID.randomUUID(), task, settableFuture, System.currentTimeMillis()));
+        } catch (IllegalStateException e) {
+            totalRejected.incrementAndGet();
+            settableFuture.setException(e);
+        }
+        return result;
+    }
+
+    public void stop() {
+        if (dispatcherExecutor != null) {
+            dispatcherExecutor.shutdownNow();
+        }
+        if (callbackExecutor != null) {
+            callbackExecutor.shutdownNow();
+        }
+        if (timeoutExecutor != null) {
+            timeoutExecutor.shutdownNow();
+        }
+    }
+
+    protected abstract SettableFuture<V> create();
+
+    protected abstract F wrap(T task, SettableFuture<V> future);
+
+    protected abstract ListenableFuture<V> execute(AsyncTaskContext<T, V> taskCtx);
+
+    private void dispatch() {
+        log.info("Buffered rate executor thread started");
+        while (!Thread.interrupted()) {
+            int curLvl = concurrencyLevel.get();
+            AsyncTaskContext<T, V> taskCtx = null;
+            try {
+                if (curLvl <= concurrencyLimit) {
+                    taskCtx = queue.take();
+                    final AsyncTaskContext<T, V> finalTaskCtx = taskCtx;
+                    logTask("Processing", finalTaskCtx);
+                    concurrencyLevel.incrementAndGet();
+                    long timeout = finalTaskCtx.getCreateTime() + maxWaitTime - System.currentTimeMillis();
+                    if (timeout > 0) {
+                        totalLaunched.incrementAndGet();
+                        ListenableFuture<V> result = execute(finalTaskCtx);
+                        result = Futures.withTimeout(result, timeout, TimeUnit.MILLISECONDS, timeoutExecutor);
+                        Futures.addCallback(result, new FutureCallback<V>() {
+                            @Override
+                            public void onSuccess(@Nullable V result) {
+                                logTask("Releasing", finalTaskCtx);
+                                totalReleased.incrementAndGet();
+                                concurrencyLevel.decrementAndGet();
+                                finalTaskCtx.getFuture().set(result);
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                if (t instanceof TimeoutException) {
+                                    logTask("Expired During Execution", finalTaskCtx);
+                                } else {
+                                    logTask("Failed", finalTaskCtx);
+                                }
+                                totalFailed.incrementAndGet();
+                                concurrencyLevel.decrementAndGet();
+                                finalTaskCtx.getFuture().setException(t);
+                                log.debug("[{}] Failed to execute task: {}", finalTaskCtx.getId(), finalTaskCtx.getTask(), t);
+                            }
+                        }, callbackExecutor);
+                    } else {
+                        logTask("Expired Before Execution", finalTaskCtx);
+                        totalExpired.incrementAndGet();
+                        concurrencyLevel.decrementAndGet();
+                        taskCtx.getFuture().setException(new TimeoutException());
+                    }
+                } else {
+                    Thread.sleep(pollMs);
+                }
+            } catch (InterruptedException e) {
+                break;
+            } catch (Throwable e) {
+                if (taskCtx != null) {
+                    log.debug("[{}] Failed to execute task: {}", taskCtx.getId(), taskCtx, e);
+                    totalFailed.incrementAndGet();
+                    concurrencyLevel.decrementAndGet();
+                } else {
+                    log.debug("Failed to queue task:", e);
+                }
+            }
+        }
+        log.info("Buffered rate executor thread stopped");
+    }
+
+    private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
+        if (log.isTraceEnabled()) {
+            log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
+        } else {
+            log.debug("[{}] {} task", taskCtx.getId(), action);
+        }
+    }
+
+    protected int getQueueSize() {
+        return queue.size();
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java
new file mode 100644
index 0000000..672e4f4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+public interface AsyncTask {
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTaskContext.java b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTaskContext.java
new file mode 100644
index 0000000..593b3fb
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTaskContext.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.SettableFuture;
+import lombok.Data;
+
+import java.util.UUID;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+@Data
+public class AsyncTaskContext<T extends AsyncTask, V> {
+
+    private final UUID id;
+    private final T task;
+    private final SettableFuture<V> future;
+    private final long createTime;
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateExecutor.java
new file mode 100644
index 0000000..21f4cd0
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateExecutor.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Created by ashvayka on 24.10.18.
+ */
+public interface BufferedRateExecutor<T extends AsyncTask, F extends ListenableFuture> {
+
+    F submit(T task);
+
+}
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 0b9c853..fc23832 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv (
 CREATE TABLE IF NOT EXISTS component_descriptor (
     id varchar(31) NOT NULL CONSTRAINT component_descriptor_pkey PRIMARY KEY,
     actions varchar(255),
-    clazz varchar,
+    clazz varchar UNIQUE,
     configuration_descriptor varchar,
     name varchar(255),
     scope varchar(255),
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
index 88f4d84..81de40a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
@@ -152,7 +152,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
     }
 
     @Test
-    public void testDeleteDeviceTsData() throws Exception {
+    public void testDeleteDeviceTsDataWithoutOverwritingLatest() throws Exception {
         DeviceId deviceId = new DeviceId(UUIDs.timeBased());
 
         saveEntries(deviceId, 10000);
@@ -172,6 +172,26 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
     }
 
     @Test
+    public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception {
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+
+        saveEntries(deviceId, 10000);
+        saveEntries(deviceId, 20000);
+        saveEntries(deviceId, 30000);
+        saveEntries(deviceId, 40000);
+
+        tsService.remove(deviceId, Collections.singletonList(
+                new BaseDeleteTsKvQuery(STRING_KEY, 25000, 45000, true))).get();
+
+        List<TsKvEntry> list = tsService.findAll(deviceId, Collections.singletonList(
+                new BaseReadTsKvQuery(STRING_KEY, 5000, 45000, 10000, 10, Aggregation.NONE))).get();
+        Assert.assertEquals(2, list.size());
+
+        List<TsKvEntry> latest = tsService.findLatest(deviceId, Collections.singletonList(STRING_KEY)).get();
+        Assert.assertEquals(20000, latest.get(0).getTs());
+    }
+
+    @Test
     public void testFindDeviceTsData() throws Exception {
         DeviceId deviceId = new DeviceId(UUIDs.timeBased());
         List<TsKvEntry> entries = new ArrayList<>();

docker/.env 25(+15 -10)

diff --git a/docker/.env b/docker/.env
index e330632..c03845d 100644
--- a/docker/.env
+++ b/docker/.env
@@ -1,13 +1,18 @@
-# cassandra environment variables
-CASSANDRA_DATA_DIR=/home/docker/cassandra_volume
 
-# postgres environment variables
-POSTGRES_DATA_DIR=/home/docker/postgres_volume
-POSTGRES_DB=thingsboard
+DOCKER_REPO=thingsboard
 
-# hsqldb environment variables
-HSQLDB_DATA_DIR=/home/docker/hsqldb_volume
+JS_EXECUTOR_DOCKER_NAME=tb-js-executor
+TB_NODE_DOCKER_NAME=tb-node
+WEB_UI_DOCKER_NAME=tb-web-ui
+MQTT_TRANSPORT_DOCKER_NAME=tb-mqtt-transport
+HTTP_TRANSPORT_DOCKER_NAME=tb-http-transport
+COAP_TRANSPORT_DOCKER_NAME=tb-coap-transport
 
-# environment variables for schema init and insert system and demo data
-ADD_SCHEMA_AND_SYSTEM_DATA=false
-ADD_DEMO_DATA=false
\ No newline at end of file
+TB_VERSION=latest
+
+# Database used by ThingsBoard, can be either postgres (PostgreSQL) or cassandra (Cassandra).
+# According to the database type corresponding docker service will be deployed (see docker-compose.postgres.yml, docker-compose.cassandra.yml for details).
+
+DATABASE=postgres
+
+KAFKA_TOPICS="js.eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.rule-engine:30:1"
diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh
new file mode 100755
index 0000000..d1dc20e
--- /dev/null
+++ b/docker/compose-utils.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+function additionalComposeArgs() {
+    source .env
+    ADDITIONAL_COMPOSE_ARGS=""
+    case $DATABASE in
+        postgres)
+        ADDITIONAL_COMPOSE_ARGS="-f docker-compose.postgres.yml"
+        ;;
+        cassandra)
+        ADDITIONAL_COMPOSE_ARGS="-f docker-compose.cassandra.yml"
+        ;;
+        *)
+        echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2
+        exit 1
+    esac
+    echo $ADDITIONAL_COMPOSE_ARGS
+}
+
+function additionalStartupServices() {
+    source .env
+    ADDITIONAL_STARTUP_SERVICES=""
+    case $DATABASE in
+        postgres)
+        ADDITIONAL_STARTUP_SERVICES=postgres
+        ;;
+        cassandra)
+        ADDITIONAL_STARTUP_SERVICES=cassandra
+        ;;
+        *)
+        echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2
+        exit 1
+    esac
+    echo $ADDITIONAL_STARTUP_SERVICES
+}
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 89a8369..1741db3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -14,40 +14,163 @@
 # limitations under the License.
 #
 
-version: '2'
+
+version: '2.2'
 
 services:
-  tb:
-    image: "thingsboard/application:2.1.0"
+  zookeeper:
+    restart: always
+    image: "zookeeper:3.5"
     ports:
-      - "8080:8080"
-      - "1883:1883"
-      - "5683:5683/udp"
+      - "2181"
+  kafka:
+    restart: always
+    image: "wurstmeister/kafka"
+    ports:
+      - "9092:9092"
     env_file:
-      - tb.env
+      - kafka.env
+    depends_on:
+      - zookeeper
+  redis:
+    image: redis:4.0
+    ports:
+      - "6379"
+  tb-js-executor:
+    restart: always
+    image: "${DOCKER_REPO}/${JS_EXECUTOR_DOCKER_NAME}:${TB_VERSION}"
+    scale: 20
+    env_file:
+      - tb-js-executor.env
+    depends_on:
+      - kafka
+  tb1:
+    restart: always
+    image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "8080"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "200m"
+        max-file: "30"
     environment:
-      - ADD_SCHEMA_AND_SYSTEM_DATA=${ADD_SCHEMA_AND_SYSTEM_DATA}
-      - ADD_DEMO_DATA=${ADD_DEMO_DATA}
+      TB_HOST: tb1
+    env_file:
+      - tb-node.env
     volumes:
-      - "${HSQLDB_DATA_DIR}:/usr/share/thingsboard/data/sql"
-    entrypoint: /run-application.sh
-  cassandra:
-    image: "cassandra:3.11.2"
+      - ./tb-node/conf:/config
+      - ./tb-node/log:/var/log/thingsboard
+    depends_on:
+      - kafka
+      - redis
+  tb2:
+    restart: always
+    image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}"
     ports:
-      - "9042"
-      - "9160"
+      - "8080"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "200m"
+        max-file: "30"
+    environment:
+      TB_HOST: tb2
+    env_file:
+      - tb-node.env
     volumes:
-      - "${CASSANDRA_DATA_DIR}:/var/lib/cassandra"
-  zk:
-    image: "zookeeper:3.4.10"
+      - ./tb-node/conf:/config
+      - ./tb-node/log:/var/log/thingsboard
+    depends_on:
+      - kafka
+      - redis
+  tb-mqtt-transport1:
+    restart: always
+    image: "${DOCKER_REPO}/${MQTT_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
     ports:
-      - "2181"
+      - "1883"
+    env_file:
+      - tb-mqtt-transport.env
+    depends_on:
+      - kafka
+  tb-mqtt-transport2:
     restart: always
-  postgres:
-    image: "postgres:9.6"
+    image: "${DOCKER_REPO}/${MQTT_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
     ports:
-    - "5432"
-    environment:
-      - POSTGRES_DB=${POSTGRES_DB}
+      - "1883"
+    env_file:
+      - tb-mqtt-transport.env
+    depends_on:
+      - kafka
+  tb-http-transport1:
+    restart: always
+    image: "${DOCKER_REPO}/${HTTP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "8081"
+    env_file:
+      - tb-http-transport.env
+    depends_on:
+      - kafka
+  tb-http-transport2:
+    restart: always
+    image: "${DOCKER_REPO}/${HTTP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "8081"
+    env_file:
+      - tb-http-transport.env
+    depends_on:
+      - kafka
+  tb-coap-transport:
+    restart: always
+    image: "${DOCKER_REPO}/${COAP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "5683:5683/udp"
+    env_file:
+      - tb-coap-transport.env
+    depends_on:
+      - kafka
+  tb-web-ui1:
+    restart: always
+    image: "${DOCKER_REPO}/${WEB_UI_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "8080"
+    env_file:
+      - tb-web-ui.env
+  tb-web-ui2:
+    restart: always
+    image: "${DOCKER_REPO}/${WEB_UI_DOCKER_NAME}:${TB_VERSION}"
+    ports:
+      - "8080"
+    env_file:
+      - tb-web-ui.env
+  haproxy:
+    restart: always
+    container_name: haproxy-certbot
+    image: xalauc/haproxy-certbot:1.7.9
     volumes:
-      - "${POSTGRES_DATA_DIR}:/var/lib/postgresql/data"
+     - ./haproxy/config:/config
+     - ./haproxy/letsencrypt:/etc/letsencrypt
+     - ./haproxy/certs.d:/usr/local/etc/haproxy/certs.d
+    ports:
+     - "80:80"
+     - "8080"
+     - "443:443"
+     - "1883:1883"
+     - "9999:9999"
+    cap_add:
+     - NET_ADMIN
+    environment:
+      HTTP_PORT: 80
+      HTTPS_PORT: 443
+      MQTT_PORT: 1883
+      TB_API_PORT: 8080
+      FORCE_HTTPS_REDIRECT: "false"
+    links:
+        - tb1
+        - tb2
+        - tb-web-ui1
+        - tb-web-ui2
+        - tb-mqtt-transport1
+        - tb-mqtt-transport2
+        - tb-http-transport1
+        - tb-http-transport2

docker/kafka.env 12(+12 -0)

diff --git a/docker/kafka.env b/docker/kafka.env
new file mode 100644
index 0000000..69fbdf6
--- /dev/null
+++ b/docker/kafka.env
@@ -0,0 +1,12 @@
+
+KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
+KAFKA_LISTENERS=INSIDE://:9093,OUTSIDE://:9092
+KAFKA_ADVERTISED_LISTENERS=INSIDE://:9093,OUTSIDE://kafka:9092
+KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
+KAFKA_INTER_BROKER_LISTENER_NAME=INSIDE
+KAFKA_CREATE_TOPICS=${KAFKA_TOPICS}
+KAFKA_AUTO_CREATE_TOPICS_ENABLE=false
+KAFKA_LOG_RETENTION_BYTES=1073741824
+KAFKA_LOG_SEGMENT_BYTES=268435456
+KAFKA_LOG_RETENTION_MS=300000
+KAFKA_LOG_CLEANUP_POLICY=delete

docker/README.md 95(+95 -0)

diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..c43a136
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,95 @@
+# Docker configuration for ThingsBoard Microservices 
+
+This folder containing scripts and Docker Compose configurations to run ThingsBoard in Microservices mode.
+
+## Prerequisites
+
+ThingsBoard Microservices are running in dockerized environment.
+Before starting please make sure [Docker CE](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed in your system.
+
+## Installation
+
+Before performing initial installation you can configure the type of database to be used with ThinsBoard.
+In order to set database type change the value of `DATABASE` variable in `.env` file to one of the following:
+
+- `postgres` - use PostgreSQL database;
+- `cassandra` - use Cassandra database;
+ 
+**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.cassandra.yml` for details).  
+
+Execute the following command to run installation:
+
+` 
+$ ./docker-install-tb.sh --loadDemo
+` 
+
+Where:
+
+- `--loadDemo` - optional argument. Whether to load additional demo data.
+
+## Running
+
+Execute the following command to start services:
+
+` 
+$ ./docker-start-services.sh
+` 
+
+After a while when all services will be successfully started you can open `http://{your-host-ip}` in you browser (for ex. `http://localhost`).
+You should see ThingsBoard login page.
+
+Use the following default credentials:
+
+- **Systen Administrator**: sysadmin@thingsboard.org / sysadmin
+
+If you installed DataBase with demo data (using `--loadDemo` flag) you can also use the following credentials:
+
+- **Tenant Administrator**: tenant@thingsboard.org / tenant
+- **Customer User**: customer@thingsboard.org / customer
+
+In case of any issues you can examine service logs for errors. 
+For example to see ThingsBoard node logs execute the following command:
+
+` 
+$ docker-compose logs -f tb1
+` 
+
+Or use `docker-compose ps` to see the state of all the containers.
+Use `docker-compose logs --f` to inspect the logs of all running services.
+See [docker-compose logs](https://docs.docker.com/compose/reference/logs/) command reference for details.  
+
+Execute the following command to stop services:
+
+` 
+$ ./docker-stop-services.sh
+` 
+
+Execute the following command to stop and completely remove deployed docker containers:
+
+` 
+$ ./docker-remove-services.sh
+` 
+
+Execute the following command to update particular or all services (pull newer docker image and rebuild container):
+
+` 
+$ ./docker-update-service.sh [SERVICE...]
+` 
+
+Where:
+
+- `[SERVICE...]` - list of services to update (defined in docker-compose configurations). If not specified all services will be updated. 
+
+## Upgrading 
+
+In case when database upgrade is needed, execute the following commands:
+
+```
+$ ./docker-stop-services.sh
+$ ./docker-upgrade-tb.sh --fromVersion=[FROM_VERSION]
+$ ./docker-start-services.sh
+```
+
+Where:
+
+- `FROM_VERSION` - from which version upgrade should be started. See [Upgrade Instructions](https://thingsboard.io/docs/user-guide/install/upgrade-instructions) for valid `fromVersion` values.
diff --git a/docker/tb-node.cassandra.env b/docker/tb-node.cassandra.env
new file mode 100644
index 0000000..8d813b9
--- /dev/null
+++ b/docker/tb-node.cassandra.env
@@ -0,0 +1,5 @@
+# ThingsBoard server configuration for Cassandra database
+
+DATABASE_TS_TYPE=cassandra
+DATABASE_ENTITIES_TYPE=cassandra
+CASSANDRA_URL=cassandra:9042

docker/tb-node.env 10(+10 -0)

diff --git a/docker/tb-node.env b/docker/tb-node.env
new file mode 100644
index 0000000..ca945ab
--- /dev/null
+++ b/docker/tb-node.env
@@ -0,0 +1,10 @@
+# ThingsBoard server configuration
+
+ZOOKEEPER_ENABLED=true
+ZOOKEEPER_URL=zookeeper:2181
+RPC_HOST=${TB_HOST}
+TB_KAFKA_SERVERS=kafka:9092
+JS_EVALUATOR=remote
+TRANSPORT_TYPE=remote
+CACHE_TYPE=redis
+REDIS_HOST=redis
diff --git a/docker/tb-node.postgres.env b/docker/tb-node.postgres.env
new file mode 100644
index 0000000..9fa79e3
--- /dev/null
+++ b/docker/tb-node.postgres.env
@@ -0,0 +1,9 @@
+# ThingsBoard server configuration for PostgreSQL database
+
+DATABASE_TS_TYPE=sql
+DATABASE_ENTITIES_TYPE=sql
+SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect
+SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver
+SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/thingsboard
+SPRING_DATASOURCE_USERNAME=postgres
+SPRING_DATASOURCE_PASSWORD=postgres
diff --git a/docker/tb-node/conf/logback.xml b/docker/tb-node/conf/logback.xml
new file mode 100644
index 0000000..1c69f53
--- /dev/null
+++ b/docker/tb-node/conf/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration scan="true" scanPeriod="10 seconds">
+
+    <appender name="fileLogAppender"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/var/log/thingsboard/${TB_HOST}/thingsboard.log</file>
+        <rollingPolicy
+                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>/var/log/thingsboard/thingsboard.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <maxFileSize>100MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>3GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard.server" level="INFO" />
+    <logger name="akka" level="INFO" />
+
+    <root level="INFO">
+        <appender-ref ref="fileLogAppender"/>
+        <appender-ref ref="STDOUT"/>
+    </root>
+
+</configuration>
\ No newline at end of file
diff --git a/docker/tb-node/conf/thingsboard.conf b/docker/tb-node/conf/thingsboard.conf
new file mode 100644
index 0000000..aa430b4
--- /dev/null
+++ b/docker/tb-node/conf/thingsboard.conf
@@ -0,0 +1,24 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data"
+export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/${TB_HOST}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
+export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
+export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
+export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
+export LOG_FILENAME=thingsboard.out
+export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions
diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml
index 6f11df1..e673a53 100644
--- a/msa/js-executor/pom.xml
+++ b/msa/js-executor/pom.xml
@@ -287,16 +287,27 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
@@ -334,6 +345,46 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
     </profiles>
     <repositories>
         <repository>

msa/pom.xml 4(+2 -2)

diff --git a/msa/pom.xml b/msa/pom.xml
index d357677..4192660 100644
--- a/msa/pom.xml
+++ b/msa/pom.xml
@@ -23,7 +23,6 @@
         <version>2.2.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
-    <groupId>org.thingsboard</groupId>
     <artifactId>msa</artifactId>
     <packaging>pom</packaging>
 
@@ -32,11 +31,12 @@
 
     <properties>
         <main.dir>${basedir}/..</main.dir>
-        <docker.repo>local-maven-build</docker.repo>
+        <docker.repo>thingsboard</docker.repo>
         <dockerfile.skip>true</dockerfile.skip>
     </properties>
 
     <modules>
+        <module>tb</module>
         <module>js-executor</module>
         <module>web-ui</module>
         <module>tb-node</module>
diff --git a/msa/tb/docker/install-tb.sh b/msa/tb/docker/install-tb.sh
new file mode 100644
index 0000000..e22ec58
--- /dev/null
+++ b/msa/tb/docker/install-tb.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+    --loadDemo)
+    LOAD_DEMO=true
+    shift # past argument
+    ;;
+    *)
+            # unknown option
+    ;;
+esac
+shift # past argument or value
+done
+
+if [ "$LOAD_DEMO" == "true" ]; then
+    loadDemo=true
+else
+    loadDemo=false
+fi
+
+CONF_FOLDER="${pkg.installFolder}/conf"
+jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
+configfile=${pkg.name}.conf
+upgradeversion=${DATA_FOLDER}/.upgradeversion
+
+source "${CONF_FOLDER}/${configfile}"
+
+echo "Starting ThingsBoard installation ..."
+
+java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
+                    -Dinstall.load_demo=${loadDemo} \
+                    -Dspring.jpa.hibernate.ddl-auto=none \
+                    -Dinstall.upgrade=false \
+                    -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \
+                    org.springframework.boot.loader.PropertiesLauncher
+
+echo "${pkg.upgradeVersion}" > ${upgradeversion}
diff --git a/msa/tb/docker/start-tb.sh b/msa/tb/docker/start-tb.sh
new file mode 100755
index 0000000..37f5e3a
--- /dev/null
+++ b/msa/tb/docker/start-tb.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+start-db.sh
+
+CONF_FOLDER="${pkg.installFolder}/conf"
+jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
+configfile=${pkg.name}.conf
+firstlaunch=${DATA_FOLDER}/.firstlaunch
+
+source "${CONF_FOLDER}/${configfile}"
+
+if [ ! -f ${firstlaunch} ]; then
+    install-tb.sh --loadDemo
+    touch ${firstlaunch}
+fi
+
+echo "Starting ThingsBoard ..."
+
+java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardServerApplication \
+                    -Dspring.jpa.hibernate.ddl-auto=none \
+                    -Dlogging.config=${CONF_FOLDER}/logback.xml \
+                    org.springframework.boot.loader.PropertiesLauncher
+
+stop-db.sh
\ No newline at end of file
diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile
new file mode 100644
index 0000000..a15408d
--- /dev/null
+++ b/msa/tb/docker-cassandra/Dockerfile
@@ -0,0 +1,59 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+FROM openjdk:8-jdk
+
+RUN apt-get update
+RUN apt-get install -y curl nmap procps
+RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
+RUN curl https://www.apache.org/dist/cassandra/KEYS | apt-key add -
+RUN apt-get update
+RUN apt-get install -y cassandra cassandra-tools
+RUN update-rc.d cassandra disable
+RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra
+
+COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/
+
+RUN chmod a+x /tmp/*.sh \
+    && mv /tmp/start-tb.sh /usr/bin \
+    && mv /tmp/upgrade-tb.sh /usr/bin \
+    && mv /tmp/install-tb.sh /usr/bin \
+    && mv /tmp/start-db.sh /usr/bin \
+    && mv /tmp/stop-db.sh /usr/bin
+
+RUN dpkg -i /tmp/${pkg.name}.deb
+
+RUN update-rc.d ${pkg.name} disable
+
+RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
+    && mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf
+
+ENV DATA_FOLDER=/data
+
+ENV HTTP_BIND_PORT=9090
+ENV DATABASE_TS_TYPE=cassandra
+ENV DATABASE_ENTITIES_TYPE=cassandra
+
+ENV CASSANDRA_HOST=localhost
+ENV CASSANDRA_PORT=9042
+
+EXPOSE 9090
+EXPOSE 1883
+EXPOSE 5683/udp
+
+VOLUME ["/data"]
+
+CMD ["start-tb.sh"]
diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile
new file mode 100644
index 0000000..c1b3f02
--- /dev/null
+++ b/msa/tb/docker-postgres/Dockerfile
@@ -0,0 +1,62 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+FROM openjdk:8-jdk
+
+RUN apt-get update
+RUN apt-get install -y postgresql postgresql-contrib
+RUN update-rc.d postgresql disable
+
+RUN mkdir -p /var/log/postgres
+RUN chown -R postgres:postgres /var/log/postgres
+
+COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/
+
+RUN chmod a+x /tmp/*.sh \
+    && mv /tmp/start-tb.sh /usr/bin \
+    && mv /tmp/upgrade-tb.sh /usr/bin \
+    && mv /tmp/install-tb.sh /usr/bin \
+    && mv /tmp/start-db.sh /usr/bin \
+    && mv /tmp/stop-db.sh /usr/bin
+
+RUN dpkg -i /tmp/${pkg.name}.deb
+
+RUN update-rc.d ${pkg.name} disable
+
+RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
+    && mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf
+
+ENV DATA_FOLDER=/data
+
+ENV HTTP_BIND_PORT=9090
+ENV DATABASE_TS_TYPE=sql
+ENV DATABASE_ENTITIES_TYPE=sql
+
+ENV PGDATA=/data/db
+
+ENV SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect
+ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver
+ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard
+ENV SPRING_DATASOURCE_USERNAME=postgres
+ENV SPRING_DATASOURCE_PASSWORD=postgres
+
+EXPOSE 9090
+EXPOSE 1883
+EXPOSE 5683/udp
+
+VOLUME ["/data"]
+
+CMD ["start-tb.sh"]
diff --git a/msa/tb/docker-tb/Dockerfile b/msa/tb/docker-tb/Dockerfile
new file mode 100644
index 0000000..497a03c
--- /dev/null
+++ b/msa/tb/docker-tb/Dockerfile
@@ -0,0 +1,48 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed 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.
+#
+
+FROM openjdk:8-jdk
+
+COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/
+
+RUN chmod a+x /tmp/*.sh \
+    && mv /tmp/start-tb.sh /usr/bin \
+    && mv /tmp/upgrade-tb.sh /usr/bin \
+    && mv /tmp/install-tb.sh /usr/bin \
+    && mv /tmp/start-db.sh /usr/bin \
+    && mv /tmp/stop-db.sh /usr/bin
+
+RUN dpkg -i /tmp/${pkg.name}.deb
+
+RUN update-rc.d ${pkg.name} disable
+
+RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
+    && mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf
+
+ENV DATA_FOLDER=/data
+
+ENV HTTP_BIND_PORT=9090
+ENV DATABASE_TS_TYPE=sql
+ENV DATABASE_ENTITIES_TYPE=sql
+ENV SQL_DATA_FOLDER=/data/db
+
+EXPOSE 9090
+EXPOSE 1883
+EXPOSE 5683/udp
+
+VOLUME ["/data"]
+
+CMD ["start-tb.sh"]

msa/tb/pom.xml 370(+370 -0)

diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml
new file mode 100644
index 0000000..6ea7beb
--- /dev/null
+++ b/msa/tb/pom.xml
@@ -0,0 +1,370 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>2.2.0-SNAPSHOT</version>
+        <artifactId>msa</artifactId>
+    </parent>
+    <groupId>org.thingsboard.msa</groupId>
+    <artifactId>tb</artifactId>
+    <packaging>pom</packaging>
+
+    <name>ThingsBoard Docker Images</name>
+    <url>https://thingsboard.io</url>
+    <description>ThingsBoard Docker Images</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+        <pkg.name>thingsboard</pkg.name>
+        <tb.docker.name>tb</tb.docker.name>
+        <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
+        <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
+        <pkg.user>thingsboard</pkg.user>
+        <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
+        <pkg.upgradeVersion>2.1.1</pkg.upgradeVersion>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>application</artifactId>
+            <version>${project.version}</version>
+            <classifier>deb</classifier>
+            <type>deb</type>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-tb-deb</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.thingsboard</groupId>
+                                    <artifactId>application</artifactId>
+                                    <classifier>deb</classifier>
+                                    <type>deb</type>
+                                    <destFileName>${pkg.name}.deb</destFileName>
+                                    <outputDirectory>${project.build.directory}/docker-tb</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-tb-postgres-deb</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.thingsboard</groupId>
+                                    <artifactId>application</artifactId>
+                                    <classifier>deb</classifier>
+                                    <type>deb</type>
+                                    <destFileName>${pkg.name}.deb</destFileName>
+                                    <outputDirectory>${project.build.directory}/docker-postgres</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-tb-cassandra-deb</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.thingsboard</groupId>
+                                    <artifactId>application</artifactId>
+                                    <classifier>deb</classifier>
+                                    <type>deb</type>
+                                    <destFileName>${pkg.name}.deb</destFileName>
+                                    <outputDirectory>${project.build.directory}/docker-cassandra</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-docker-tb-config</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/docker-tb</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>docker</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                                <resource>
+                                    <directory>docker-tb</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-docker-tb-postgres-config</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/docker-postgres</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>docker</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                                <resource>
+                                    <directory>docker-postgres</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-docker-tb-cassandra-config</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/docker-cassandra</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>docker</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                                <resource>
+                                    <directory>docker-cassandra</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>dockerfile-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>build-docker-tb-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>build</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb.docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}/docker-tb</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-tb-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb.docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>build-docker-tb-postgres-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>build</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb-postgres.docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}/docker-postgres</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-tb-postgres-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb-postgres.docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>build-docker-tb-cassandra-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>build</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb-cassandra.docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}/docker-cassandra</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-tb-cassandra-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${tb-cassandra.docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+    <profiles>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-tb-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${tb.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-tb-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${tb.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-latest-docker-tb-postgres-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${tb-postgres.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-tb-postgres-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${tb-postgres.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-latest-docker-tb-cassandra-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${tb-cassandra.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-tb-cassandra-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${tb-cassandra.docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+    <repositories>
+        <repository>
+            <id>jenkins</id>
+            <name>Jenkins Repository</name>
+            <url>http://repo.jenkins-ci.org/releases</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+</project>

msa/tb/README.md 83(+83 -0)

diff --git a/msa/tb/README.md b/msa/tb/README.md
new file mode 100644
index 0000000..e4b7400
--- /dev/null
+++ b/msa/tb/README.md
@@ -0,0 +1,83 @@
+# ThingsBoard single docker images 
+
+This project provides the build for the ThingsBoard single docker images.
+
+* `thingsboard/tb` - single instance of ThingsBoard with embedded HSQLDB database.
+* `thingsboard/tb-postgres` - single instance of ThingsBoard with PostgreSQL database.
+* `thingsboard/tb-cassandra` - single instance of ThingsBoard with Cassandra database.
+
+## Running
+
+In this example `thingsboard/tb` image will be used. You can choose any other images with different databases (see above).
+Execute the following command to run this docker directly:
+
+` 
+$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb
+` 
+
+Where: 
+    
+- `docker run`              - run this container
+- `-it`                     - attach a terminal session with current ThingsBoard process output
+- `-p 9090:9090`            - connect local port 9090 to exposed internal HTTP port 9090
+- `-p 1883:1883`            - connect local port 1883 to exposed internal MQTT port 1883    
+- `-p 5683:5683`            - connect local port 5683 to exposed internal COAP port 5683 
+- `-v ~/.mytb-data:/data`   - mounts the host's dir `~/.mytb-data` to ThingsBoard DataBase data directory
+- `--name mytb`             - friendly local name of this machine
+- `thingsboard/tb`          - docker image, can be also `thingsboard/tb-postgres` or `thingsboard/tb-cassandra`
+
+> **NOTE**: **Windows** users should use docker managed volume instead of host's dir. Create docker volume (for ex. `mytb-data`) before executing `docker run` command:
+> ```
+> $ docker create volume mytb-data
+> ```
+> After you can execute docker run command using `mytb-data` volume instead of `~/.mytb-data`.
+> In order to get access to necessary resources from external IP/Host on **Windows** machine, please execute the following commands:
+> ```
+> $ VBoxManage controlvm "default" natpf1 "tcp-port9090,tcp,,9090,,9090"  
+> $ VBoxManage controlvm "default" natpf1 "tcp-port1883,tcp,,1883,,1883"
+> $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5683,,5683"
+> ```
+
+After executing `docker run` command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page.
+Use the following default credentials:
+
+- **System Administrator**: sysadmin@thingsboard.org / sysadmin
+- **Tenant Administrator**: tenant@thingsboard.org / tenant
+- **Customer User**: customer@thingsboard.org / customer
+    
+You can always change passwords for each account in account profile page.
+
+You can detach from session terminal with `Ctrl-p` `Ctrl-q` - the container will keep running in the background.
+
+To reattach to the terminal (to see ThingsBoard logs) run:
+
+```
+$ docker attach mytb
+```
+
+To stop the container:
+
+```
+$ docker stop mytb
+```
+
+To start the container:
+
+```
+$ docker start mytb
+```
+
+## Upgrading
+
+In order to update to the latest image, execute the following commands:
+
+```
+$ docker pull thingsboard/tb
+$ docker stop mytb
+$ docker run -it -v ~/.mytb-data:/data --rm thingsboard/tb upgrade-tb.sh
+$ docker start mytb
+```
+
+**NOTE**: if you use different database change image name in all commands from `thingsboard/tb` to `thingsboard/tb-postgres` or `thingsboard/tb-cassandra` correspondingly.
+ 
+**NOTE**: replace host's directory `~/.mytb-data` with directory used during container creation. 
diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile
index c86d020..57a6151 100644
--- a/msa/tb-node/docker/Dockerfile
+++ b/msa/tb-node/docker/Dockerfile
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-FROM openjdk:8-jre
+FROM openjdk:8-jdk
 
 COPY start-tb-node.sh ${pkg.name}.deb /tmp/
 

msa/tb-node/pom.xml 69(+61 -8)

diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml
index 18a81f3..fc62189 100644
--- a/msa/tb-node/pom.xml
+++ b/msa/tb-node/pom.xml
@@ -111,19 +111,72 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
     <repositories>
         <repository>
             <id>jenkins</id>
diff --git a/msa/transport/coap/docker/Dockerfile b/msa/transport/coap/docker/Dockerfile
index dcaef3a..9240b2a 100644
--- a/msa/transport/coap/docker/Dockerfile
+++ b/msa/transport/coap/docker/Dockerfile
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-FROM openjdk:8-jre
+FROM openjdk:8-jdk
 
 COPY logback.xml ${pkg.name}.conf start-tb-coap-transport.sh ${pkg.name}.deb /tmp/
 
diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml
index 94399c7..26eaa30 100644
--- a/msa/transport/coap/pom.xml
+++ b/msa/transport/coap/pom.xml
@@ -111,19 +111,72 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
     <repositories>
         <repository>
             <id>jenkins</id>
diff --git a/msa/transport/http/docker/Dockerfile b/msa/transport/http/docker/Dockerfile
index 212047f..6c83b9c 100644
--- a/msa/transport/http/docker/Dockerfile
+++ b/msa/transport/http/docker/Dockerfile
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-FROM openjdk:8-jre
+FROM openjdk:8-jdk
 
 COPY logback.xml ${pkg.name}.conf start-tb-http-transport.sh ${pkg.name}.deb /tmp/
 
diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml
index 6bbfc3f..6d1707c 100644
--- a/msa/transport/http/pom.xml
+++ b/msa/transport/http/pom.xml
@@ -111,19 +111,72 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
     <repositories>
         <repository>
             <id>jenkins</id>
diff --git a/msa/transport/mqtt/docker/Dockerfile b/msa/transport/mqtt/docker/Dockerfile
index 5827c65..f636e2f 100644
--- a/msa/transport/mqtt/docker/Dockerfile
+++ b/msa/transport/mqtt/docker/Dockerfile
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-FROM openjdk:8-jre
+FROM openjdk:8-jdk
 
 COPY logback.xml ${pkg.name}.conf start-tb-mqtt-transport.sh ${pkg.name}.deb /tmp/
 
diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml
index 0a3b077..d51c51c 100644
--- a/msa/transport/mqtt/pom.xml
+++ b/msa/transport/mqtt/pom.xml
@@ -111,19 +111,72 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
     <repositories>
         <repository>
             <id>jenkins</id>

msa/web-ui/pom.xml 67(+59 -8)

diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml
index 805bf8b..83124df 100644
--- a/msa/web-ui/pom.xml
+++ b/msa/web-ui/pom.xml
@@ -311,16 +311,27 @@
                         <goals>
                             <goal>build</goal>
                         </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <verbose>true</verbose>
+                            <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                            <contextDirectory>${project.build.directory}</contextDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tag-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>tag</goal>
+                        </goals>
+                        <configuration>
+                            <skip>${dockerfile.skip}</skip>
+                            <repository>${docker.repo}/${docker.name}</repository>
+                            <tag>${project.version}</tag>
+                        </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <skip>${dockerfile.skip}</skip>
-                    <repository>${docker.repo}/${docker.name}</repository>
-                    <tag>${project.version}</tag>
-                    <verbose>true</verbose>
-                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
-                    <contextDirectory>${project.build.directory}</contextDirectory>
-                </configuration>
             </plugin>
         </plugins>
     </build>
@@ -358,6 +369,46 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>push-docker-image</id>
+            <activation>
+                <property>
+                    <name>push-docker-image</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.spotify</groupId>
+                        <artifactId>dockerfile-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>push-latest-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>latest</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>push-version-docker-image</id>
+                                <phase>pre-integration-test</phase>
+                                <goals>
+                                    <goal>push</goal>
+                                </goals>
+                                <configuration>
+                                    <tag>${project.version}</tag>
+                                    <repository>${docker.repo}/${docker.name}</repository>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
     </profiles>
     <repositories>
         <repository>
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
index 9a93d1e..f3d57de 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
@@ -31,6 +31,8 @@ public final class RuleEngineDeviceRpcRequest {
     private final DeviceId deviceId;
     private final int requestId;
     private final UUID requestUUID;
+    private final String originHost;
+    private final int originPort;
     private final boolean oneway;
     private final String method;
     private final String body;
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
index c568e3d..e3766ba 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
@@ -37,7 +37,7 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgG
         configuration.setPeriodInSeconds(1);
         configuration.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
                 "var metadata = { data: 40 };\n" +
-                "var msgType = \"DebugMsg\";\n\n" +
+                "var msgType = \"POST_TELEMETRY_REQUEST\";\n\n" +
                 "return { msg: msg, metadata: metadata, msgType: msgType };");
         return configuration;
     }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
index 5d7e124..8c95b88 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
@@ -86,6 +86,10 @@ public class TbSendRPCRequestNode implements TbNode {
 
             tmp = msg.getMetaData().getValue("requestUUID");
             UUID requestUUID = !StringUtils.isEmpty(tmp) ? UUID.fromString(tmp) : UUIDs.timeBased();
+            tmp = msg.getMetaData().getValue("originHost");
+            String originHost = !StringUtils.isEmpty(tmp) ? tmp : null;
+            tmp = msg.getMetaData().getValue("originPort");
+            int originPort = !StringUtils.isEmpty(tmp) ? Integer.parseInt(tmp) : 0;
 
             tmp = msg.getMetaData().getValue("expirationTime");
             long expirationTime = !StringUtils.isEmpty(tmp) ? Long.parseLong(tmp) : (System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(config.getTimeoutInSeconds()));
@@ -105,6 +109,8 @@ public class TbSendRPCRequestNode implements TbNode {
                     .deviceId(new DeviceId(msg.getOriginator().getId()))
                     .requestId(requestId)
                     .requestUUID(requestUUID)
+                    .originHost(originHost)
+                    .originPort(originPort)
                     .expirationTime(expirationTime)
                     .restApiCall(restApiCall)
                     .build();
diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml
index 30351b0..f96bcbe 100644
--- a/transport/coap/src/main/resources/tb-coap-transport.yml
+++ b/transport/coap/src/main/resources/tb-coap-transport.yml
@@ -23,6 +23,9 @@ transport:
     bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
     bind_port: "${COAP_BIND_PORT:5683}"
     timeout: "${COAP_TIMEOUT:10000}"
+  sessions:
+    inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
+    report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
   rate_limits:
     enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
     tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml
index 001e08a..6d593ed 100644
--- a/transport/http/src/main/resources/tb-http-transport.yml
+++ b/transport/http/src/main/resources/tb-http-transport.yml
@@ -24,6 +24,9 @@ server:
 transport:
   http:
     request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
+  sessions:
+    inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
+    report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
   rate_limits:
     enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
     tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
index e37d14e..e7f8942 100644
--- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
+++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
@@ -44,8 +44,8 @@ transport:
       # Type of the key store
       key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
   sessions:
-    max_per_tenant: "${TB_TRANSPORT_SESSIONS_MAX_PER_TENANT:1000}"
-    max_per_device: "${TB_TRANSPORT_SESSIONS_MAX_PER_DEVICE:2}"
+    inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
+    report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
   rate_limits:
     enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
     tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"

ui/package.json 4(+2 -2)

diff --git a/ui/package.json b/ui/package.json
index 7874170..9fb0cc5 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -26,7 +26,7 @@
     "angular-gridster": "^0.13.14",
     "angular-hotkeys": "^1.7.0",
     "angular-jwt": "^0.1.6",
-    "angular-material": "1.1.1",
+    "angular-material": "1.1.9",
     "angular-material-data-table": "^0.10.9",
     "angular-material-icons": "^0.7.1",
     "angular-material-expansion-panel": "^0.7.2",
@@ -63,7 +63,7 @@
     "leaflet-providers": "^1.1.17",
     "material-ui": "^0.16.1",
     "material-ui-number-input": "^5.0.16",
-    "md-color-picker": "^0.2.6",
+    "md-color-picker": "0.2.6",
     "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
     "moment": "^2.15.0",
     "ngclipboard": "^1.1.1",
diff --git a/ui/src/app/alarm/alarm-row.directive.js b/ui/src/app/alarm/alarm-row.directive.js
index 37eec7c..681498f 100644
--- a/ui/src/app/alarm/alarm-row.directive.js
+++ b/ui/src/app/alarm/alarm-row.directive.js
@@ -50,7 +50,7 @@ export default function AlarmRowDirective($compile, $templateCache, types, $mdDi
                 parent: angular.element($document[0].body),
                 targetEvent: $event,
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 onShowing: function(scope, element) {
                     onShowingCallback.onShowing(scope, element);
                 }

ui/src/app/app.js 32(+18 -14)

diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index a3a179e..31b53a0 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -31,6 +31,7 @@ import 'angular-translate-interpolation-messageformat';
 import 'md-color-picker';
 import mdPickers from 'mdPickers';
 import ngSanitize from 'angular-sanitize';
+import FBAngular from 'angular-fullscreen';
 import vAccordion from 'v-accordion';
 import ngAnimate from 'angular-animate';
 import 'angular-websocket';
@@ -51,6 +52,21 @@ import react from 'ngreact';
 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
 import 'ngFlowchart/dist/ngFlowchart';
 
+import 'typeface-roboto';
+import 'font-awesome/css/font-awesome.min.css';
+import 'angular-material/angular-material.min.css';
+import 'angular-material-icons/angular-material-icons.css';
+import 'angular-gridster/dist/angular-gridster.min.css';
+import 'v-accordion/dist/v-accordion.min.css';
+import 'md-color-picker/dist/mdColorPicker.min.css';
+import 'mdPickers/dist/mdPickers.min.css';
+import 'angular-hotkeys/build/hotkeys.min.css';
+import 'angular-carousel/dist/angular-carousel.min.css';
+import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
+import '../scss/main.scss';
+
+import thingsboardThirdpartyFix from './common/thirdparty-fix';
 import thingsboardTranslateHandler from './locale/translate-handler';
 import thingsboardLogin from './login';
 import thingsboardDialogs from './components/datakey-config-dialog.controller';
@@ -78,20 +94,6 @@ import thingsboardApiAuditLog from './api/audit-log.service';
 import thingsboardApiComponentDescriptor from './api/component-descriptor.service';
 import thingsboardApiRuleChain from './api/rule-chain.service';
 
-import 'typeface-roboto';
-import 'font-awesome/css/font-awesome.min.css';
-import 'angular-material/angular-material.min.css';
-import 'angular-material-icons/angular-material-icons.css';
-import 'angular-gridster/dist/angular-gridster.min.css';
-import 'v-accordion/dist/v-accordion.min.css';
-import 'md-color-picker/dist/mdColorPicker.min.css';
-import 'mdPickers/dist/mdPickers.min.css';
-import 'angular-hotkeys/build/hotkeys.min.css';
-import 'angular-carousel/dist/angular-carousel.min.css';
-import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
-import 'ngFlowchart/dist/flowchart.css';
-import '../scss/main.scss';
-
 import AppConfig from './app.config';
 import GlobalInterceptor from './global-interceptor.service';
 import AppRun from './app.run';
@@ -105,6 +107,7 @@ angular.module('thingsboard', [
     'mdColorPicker',
     mdPickers,
     ngSanitize,
+    FBAngular.name,
     vAccordion,
     ngAnimate,
     'ngWebSocket',
@@ -118,6 +121,7 @@ angular.module('thingsboard', [
     react.name,
     'flow',
     'flowchart',
+    thingsboardThirdpartyFix,
     thingsboardTranslateHandler,
     thingsboardLogin,
     thingsboardDialogs,
diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js
index 2c2e170..f13a0d2 100644
--- a/ui/src/app/audit/audit-log-row.directive.js
+++ b/ui/src/app/audit/audit-log-row.directive.js
@@ -48,7 +48,7 @@ export default function AuditLogRowDirective($compile, $templateCache, types, $m
                 parent: angular.element($document[0].body),
                 targetEvent: $event,
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 onShowing: function(scope, element) {
                     onShowingCallback.onShowing(scope, element);
                 }
diff --git a/ui/src/app/common/thirdparty-fix.js b/ui/src/app/common/thirdparty-fix.js
new file mode 100644
index 0000000..af4222c
--- /dev/null
+++ b/ui/src/app/common/thirdparty-fix.js
@@ -0,0 +1,459 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+import tinycolor from 'tinycolor2';
+import moment from 'moment';
+
+export default angular.module('thingsboard.thirdpartyFix', [])
+    .factory('Fullscreen', Fullscreen)
+    .factory('$mdColorPicker', mdColorPicker)
+    .provider('$mdpDatePicker', mdpDatePicker)
+    .provider('$mdpTimePicker', mdpTimePicker)
+    .name;
+
+/*@ngInject*/
+function Fullscreen($document, $rootScope) {
+
+    /* eslint-disable */
+
+    var document = $document[0];
+
+    // ensure ALLOW_KEYBOARD_INPUT is available and enabled
+    var isKeyboardAvailbleOnFullScreen = (typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element) && Element.ALLOW_KEYBOARD_INPUT;
+
+    var emitter = $rootScope.$new();
+
+    // listen event on document instead of element to avoid firefox limitation
+    // see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode
+    $document.on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function(){
+        emitter.$emit('FBFullscreen.change', serviceInstance.isEnabled());
+    });
+
+    var serviceInstance = {
+        $on: angular.bind(emitter, emitter.$on),
+        all: function() {
+            serviceInstance.enable( document.documentElement );
+        },
+        enable: function(element) {
+            if(element.requestFullScreen) {
+                element.requestFullScreen();
+            } else if(element.mozRequestFullScreen) {
+                element.mozRequestFullScreen();
+            } else if(element.webkitRequestFullscreen) {
+                // Safari temporary fix
+                //if (/Version\/[\d]{1,2}(\.[\d]{1,2}){1}(\.(\d){1,2}){0,1} Safari/.test(navigator.userAgent)) {
+                if (/Safari/.test(navigator.userAgent)) {
+                    element.webkitRequestFullscreen();
+                } else {
+                    element.webkitRequestFullscreen(isKeyboardAvailbleOnFullScreen);
+                }
+            } else if (element.msRequestFullscreen) {
+                element.msRequestFullscreen();
+            }
+        },
+        cancel: function() {
+            if(document.cancelFullScreen) {
+                document.cancelFullScreen();
+            } else if(document.mozCancelFullScreen) {
+                document.mozCancelFullScreen();
+            } else if(document.webkitExitFullscreen) {
+                document.webkitExitFullscreen();
+            } else if (document.msExitFullscreen) {
+                document.msExitFullscreen();
+            }
+        },
+        isEnabled: function(){
+            var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
+            return fullscreenElement ? true : false;
+        },
+        toggleAll: function(){
+            serviceInstance.isEnabled() ? serviceInstance.cancel() : serviceInstance.all();
+        },
+        isSupported: function(){
+            var docElm = document.documentElement;
+            var requestFullscreen = docElm.requestFullScreen || docElm.mozRequestFullScreen || docElm.webkitRequestFullscreen || docElm.msRequestFullscreen;
+            return requestFullscreen ? true : false;
+        }
+    };
+
+    /* eslint-enable */
+
+    return serviceInstance;
+}
+
+/*@ngInject*/
+function mdColorPicker($q, $mdDialog, mdColorPickerHistory) {
+    var dialog;
+
+    /* eslint-disable angular/definedundefined */
+
+    return {
+        show: function (options)
+        {
+            if ( options === undefined ) {
+                options = {};
+            }
+            //console.log( 'DIALOG OPTIONS', options );
+            // Defaults
+            // Dialog Properties
+            options.hasBackdrop = options.hasBackdrop === undefined ? true : options.hasBackdrop;
+            options.clickOutsideToClose = options.clickOutsideToClose === undefined ? true : options.clickOutsideToClose;
+            options.defaultValue = options.defaultValue === undefined ? '#FFFFFF' : options.defaultValue;
+            options.focusOnOpen = options.focusOnOpen === undefined ? false : options.focusOnOpen;
+            options.preserveScope = options.preserveScope === undefined ? true : options.preserveScope;
+            if (options.skipHide !== undefined) {
+                options.multiple = options.skipHide;
+            }
+            if (options.multiple === undefined) {
+                options.multiple = true;
+            }
+
+            // mdColorPicker Properties
+            options.mdColorAlphaChannel = options.mdColorAlphaChannel === undefined ? false : options.mdColorAlphaChannel;
+            options.mdColorSpectrum = options.mdColorSpectrum === undefined ? true : options.mdColorSpectrum;
+            options.mdColorSliders = options.mdColorSliders === undefined ? true : options.mdColorSliders;
+            options.mdColorGenericPalette = options.mdColorGenericPalette === undefined ? true : options.mdColorGenericPalette;
+            options.mdColorMaterialPalette = options.mdColorMaterialPalette === undefined ? true : options.mdColorMaterialPalette;
+            options.mdColorHistory = options.mdColorHistory === undefined ? true : options.mdColorHistory;
+
+
+            dialog = $mdDialog.show({
+                templateUrl: 'mdColorPickerDialog.tpl.html',
+                hasBackdrop: options.hasBackdrop,
+                clickOutsideToClose: options.clickOutsideToClose,
+
+                controller: ['$scope', 'options', function( $scope, options ) {
+                    //console.log( "DIALOG CONTROLLER OPEN", Date.now() - dateClick );
+                    $scope.close = function close()
+                    {
+                        $mdDialog.cancel();
+                    };
+                    $scope.ok = function ok()
+                    {
+                        $mdDialog.hide( $scope.value );
+                    };
+                    $scope.hide = $scope.ok;
+
+
+
+                    $scope.value = options.value;
+                    $scope.default = options.defaultValue;
+                    $scope.random = options.random;
+
+                    $scope.mdColorAlphaChannel = options.mdColorAlphaChannel;
+                    $scope.mdColorSpectrum = options.mdColorSpectrum;
+                    $scope.mdColorSliders = options.mdColorSliders;
+                    $scope.mdColorGenericPalette = options.mdColorGenericPalette;
+                    $scope.mdColorMaterialPalette = options.mdColorMaterialPalette;
+                    $scope.mdColorHistory = options.mdColorHistory;
+                    $scope.mdColorDefaultTab = options.mdColorDefaultTab;
+
+                }],
+
+                locals: {
+                    options: options,
+                },
+                preserveScope: options.preserveScope,
+                multiple: options.multiple,
+
+                targetEvent: options.$event,
+                focusOnOpen: options.focusOnOpen,
+                autoWrap: false,
+                onShowing: function() {
+                    //		console.log( "DIALOG OPEN START", Date.now() - dateClick );
+                },
+                onComplete: function() {
+                    //		console.log( "DIALOG OPEN COMPLETE", Date.now() - dateClick );
+                }
+            });
+
+            dialog.then(function (value) {
+                mdColorPickerHistory.add(new tinycolor(value));
+            }, function () { });
+
+            return dialog;
+        },
+        hide: function() {
+            return dialog.hide();
+        },
+        cancel: function() {
+            return dialog.cancel();
+        }
+    };
+
+    /* eslint-enable angular/definedundefined */
+}
+
+function DatePickerCtrl($scope, $mdDialog, $mdMedia, $timeout, currentDate, options) {
+    var self = this;
+
+    this.date = moment(currentDate);
+    this.minDate = options.minDate && moment(options.minDate).isValid() ? moment(options.minDate) : null;
+    this.maxDate = options.maxDate && moment(options.maxDate).isValid() ? moment(options.maxDate) : null;
+    this.displayFormat = options.displayFormat || "ddd, MMM DD";
+    this.dateFilter = angular.isFunction(options.dateFilter) ? options.dateFilter : null;
+    this.selectingYear = false;
+
+    // validate min and max date
+    if (this.minDate && this.maxDate) {
+        if (this.maxDate.isBefore(this.minDate)) {
+            this.maxDate = moment(this.minDate).add(1, 'days');
+        }
+    }
+
+    if (this.date) {
+        // check min date
+        if (this.minDate && this.date.isBefore(this.minDate)) {
+            this.date = moment(this.minDate);
+        }
+
+        // check max date
+        if (this.maxDate && this.date.isAfter(this.maxDate)) {
+            this.date = moment(this.maxDate);
+        }
+    }
+
+    this.yearItems = {
+        currentIndex_: 0,
+        PAGE_SIZE: 5,
+        START: (self.minDate ? self.minDate.year() : 1900),
+        END: (self.maxDate ? self.maxDate.year() : 0),
+        getItemAtIndex: function(index) {
+            if(this.currentIndex_ < index)
+                this.currentIndex_ = index;
+
+            return this.START + index;
+        },
+        getLength: function() {
+            return Math.min(
+                this.currentIndex_ + Math.floor(this.PAGE_SIZE / 2),
+                Math.abs(this.START - this.END) + 1
+            );
+        }
+    };
+
+    $scope.$mdMedia = $mdMedia;
+    $scope.year = this.date.year();
+
+    this.selectYear = function(year) {
+        self.date.year(year);
+        $scope.year = year;
+        self.selectingYear = false;
+        self.animate();
+    };
+
+    this.showYear = function() {
+        self.yearTopIndex = (self.date.year() - self.yearItems.START) + Math.floor(self.yearItems.PAGE_SIZE / 2);
+        self.yearItems.currentIndex_ = (self.date.year() - self.yearItems.START) + 1;
+        self.selectingYear = true;
+    };
+
+    this.showCalendar = function() {
+        self.selectingYear = false;
+    };
+
+    this.cancel = function() {
+        $mdDialog.cancel();
+    };
+
+    this.confirm = function() {
+        var date = this.date;
+
+        if (this.minDate && this.date.isBefore(this.minDate)) {
+            date = moment(this.minDate);
+        }
+
+        if (this.maxDate && this.date.isAfter(this.maxDate)) {
+            date = moment(this.maxDate);
+        }
+
+        $mdDialog.hide(date.toDate());
+    };
+
+    this.animate = function() {
+        self.animating = true;
+        $timeout(angular.noop).then(function() {
+            self.animating = false;
+        })
+    };
+}
+
+/*@ngInject*/
+function mdpDatePicker() {
+    var LABEL_OK = "OK",
+        LABEL_CANCEL = "Cancel",
+        DISPLAY_FORMAT = "ddd, MMM DD";
+
+    this.setDisplayFormat = function(format) {
+        DISPLAY_FORMAT = format;
+    };
+
+    this.setOKButtonLabel = function(label) {
+        LABEL_OK = label;
+    };
+
+    this.setCancelButtonLabel = function(label) {
+        LABEL_CANCEL = label;
+    };
+
+    /*@ngInject*/
+    this.$get = function($mdDialog) {
+        var datePicker = function(currentDate, options) {
+            if (!angular.isDate(currentDate)) currentDate = Date.now();
+            if (!angular.isObject(options)) options = {};
+
+            options.displayFormat = DISPLAY_FORMAT;
+
+            return $mdDialog.show({
+                controller:  ['$scope', '$mdDialog', '$mdMedia', '$timeout', 'currentDate', 'options', DatePickerCtrl],
+                controllerAs: 'datepicker',
+                clickOutsideToClose: true,
+                template: '<md-dialog aria-label="" class="mdp-datepicker" ng-class="{ \'portrait\': !$mdMedia(\'gt-xs\') }">' +
+                '<md-dialog-content layout="row" layout-wrap>' +
+                '<div layout="column" layout-align="start center">' +
+                '<md-toolbar layout-align="start start" flex class="mdp-datepicker-date-wrapper md-hue-1 md-primary" layout="column">' +
+                '<span class="mdp-datepicker-year" ng-click="datepicker.showYear()" ng-class="{ \'active\': datepicker.selectingYear }">{{ datepicker.date.format(\'YYYY\') }}</span>' +
+                '<span class="mdp-datepicker-date" ng-click="datepicker.showCalendar()" ng-class="{ \'active\': !datepicker.selectingYear }">{{ datepicker.date.format(datepicker.displayFormat) }}</span> ' +
+                '</md-toolbar>' +
+                '</div>' +
+                '<div>' +
+                '<div class="mdp-datepicker-select-year mdp-animation-zoom" layout="column" layout-align="center start" ng-if="datepicker.selectingYear">' +
+                '<md-virtual-repeat-container md-auto-shrink md-top-index="datepicker.yearTopIndex">' +
+                '<div flex md-virtual-repeat="item in datepicker.yearItems" md-on-demand class="repeated-year">' +
+                '<span class="md-button" ng-click="datepicker.selectYear(item)" md-ink-ripple ng-class="{ \'md-primary current\': item == year }">{{ item }}</span>' +
+                '</div>' +
+                '</md-virtual-repeat-container>' +
+                '</div>' +
+                '<mdp-calendar ng-if="!datepicker.selectingYear" class="mdp-animation-zoom" date="datepicker.date" min-date="datepicker.minDate" date-filter="datepicker.dateFilter" max-date="datepicker.maxDate"></mdp-calendar>' +
+                '<md-dialog-actions layout="row">' +
+                '<span flex></span>' +
+                '<md-button ng-click="datepicker.cancel()" aria-label="' + LABEL_CANCEL + '">' + LABEL_CANCEL + '</md-button>' +
+                '<md-button ng-click="datepicker.confirm()" class="md-primary" aria-label="' + LABEL_OK + '">' + LABEL_OK + '</md-button>' +
+                '</md-dialog-actions>' +
+                '</div>' +
+                '</md-dialog-content>' +
+                '</md-dialog>',
+                targetEvent: options.targetEvent,
+                locals: {
+                    currentDate: currentDate,
+                    options: options
+                },
+                multiple: true
+            });
+        };
+
+        return datePicker;
+    };
+
+}
+
+function TimePickerCtrl($scope, $mdDialog, time, autoSwitch, $mdMedia) {
+    var self = this;
+    this.VIEW_HOURS = 1;
+    this.VIEW_MINUTES = 2;
+    this.currentView = this.VIEW_HOURS;
+    this.time = moment(time);
+    this.autoSwitch = !!autoSwitch;
+
+    this.clockHours = parseInt(this.time.format("h"));
+    this.clockMinutes = parseInt(this.time.minutes());
+
+    $scope.$mdMedia = $mdMedia;
+
+    this.switchView = function() {
+        self.currentView = self.currentView == self.VIEW_HOURS ? self.VIEW_MINUTES : self.VIEW_HOURS;
+    };
+
+    this.setAM = function() {
+        if(self.time.hours() >= 12)
+            self.time.hour(self.time.hour() - 12);
+    };
+
+    this.setPM = function() {
+        if(self.time.hours() < 12)
+            self.time.hour(self.time.hour() + 12);
+    };
+
+    this.cancel = function() {
+        $mdDialog.cancel();
+    };
+
+    this.confirm = function() {
+        $mdDialog.hide(this.time.toDate());
+    };
+}
+
+/*@ngInject*/
+function mdpTimePicker() {
+    var LABEL_OK = "OK",
+        LABEL_CANCEL = "Cancel";
+
+    this.setOKButtonLabel = function(label) {
+        LABEL_OK = label;
+    };
+
+    this.setCancelButtonLabel = function(label) {
+        LABEL_CANCEL = label;
+    };
+
+    /*@ngInject*/
+    this.$get = function($mdDialog) {
+        var timePicker = function(time, options) {
+            if(!angular.isDate(time)) time = Date.now();
+            if (!angular.isObject(options)) options = {};
+
+            return $mdDialog.show({
+                controller:  ['$scope', '$mdDialog', 'time', 'autoSwitch', '$mdMedia', TimePickerCtrl],
+                controllerAs: 'timepicker',
+                clickOutsideToClose: true,
+                template: '<md-dialog aria-label="" class="mdp-timepicker" ng-class="{ \'portrait\': !$mdMedia(\'gt-xs\') }">' +
+                '<md-dialog-content layout-gt-xs="row" layout-wrap>' +
+                '<md-toolbar layout-gt-xs="column" layout-xs="row" layout-align="center center" flex class="mdp-timepicker-time md-hue-1 md-primary">' +
+                '<div class="mdp-timepicker-selected-time">' +
+                '<span ng-class="{ \'active\': timepicker.currentView == timepicker.VIEW_HOURS }" ng-click="timepicker.currentView = timepicker.VIEW_HOURS">{{ timepicker.time.format("h") }}</span>:' +
+                '<span ng-class="{ \'active\': timepicker.currentView == timepicker.VIEW_MINUTES }" ng-click="timepicker.currentView = timepicker.VIEW_MINUTES">{{ timepicker.time.format("mm") }}</span>' +
+                '</div>' +
+                '<div layout="column" class="mdp-timepicker-selected-ampm">' +
+                '<span ng-click="timepicker.setAM()" ng-class="{ \'active\': timepicker.time.hours() < 12 }">AM</span>' +
+                '<span ng-click="timepicker.setPM()" ng-class="{ \'active\': timepicker.time.hours() >= 12 }">PM</span>' +
+                '</div>' +
+                '</md-toolbar>' +
+                '<div>' +
+                '<div class="mdp-clock-switch-container" ng-switch="timepicker.currentView" layout layout-align="center center">' +
+                '<mdp-clock class="mdp-animation-zoom" auto-switch="timepicker.autoSwitch" time="timepicker.time" type="hours" ng-switch-when="1"></mdp-clock>' +
+                '<mdp-clock class="mdp-animation-zoom" auto-switch="timepicker.autoSwitch" time="timepicker.time" type="minutes" ng-switch-when="2"></mdp-clock>' +
+                '</div>' +
+
+                '<md-dialog-actions layout="row">' +
+                '<span flex></span>' +
+                '<md-button ng-click="timepicker.cancel()" aria-label="' + LABEL_CANCEL + '">' + LABEL_CANCEL + '</md-button>' +
+                '<md-button ng-click="timepicker.confirm()" class="md-primary" aria-label="' + LABEL_OK + '">' + LABEL_OK + '</md-button>' +
+                '</md-dialog-actions>' +
+                '</div>' +
+                '</md-dialog-content>' +
+                '</md-dialog>',
+                targetEvent: options.targetEvent,
+                locals: {
+                    time: time,
+                    autoSwitch: options.autoSwitch
+                },
+                multiple: true
+            });
+        };
+
+        return timePicker;
+    };
+}
diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js
index b02a30d..6bc1932 100644
--- a/ui/src/app/components/datasource-entity.directive.js
+++ b/ui/src/app/components/datasource-entity.directive.js
@@ -186,7 +186,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
                 random: tinycolor.random(),
                 clickOutsideToClose: false,
                 hasBackdrop: false,
-                skipHide: true,
+                multiple: true,
                 preserveScope: false,
 
                 mdColorAlphaChannel: true,
@@ -220,7 +220,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: event,
-                skipHide: true,
+                multiple: true,
                 onComplete: function () {
                     var w = angular.element($window);
                     w.triggerHandler('resize');
diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html
index db6fd3b..8f25787 100644
--- a/ui/src/app/components/datasource-entity.tpl.html
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -26,7 +26,6 @@
 		   <section flex layout='column' layout-align="center" style="padding-left: 4px;">
 			   <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
 						 id="timeseries_datakey_chips"
-						 ng-required="true"
 						 ng-model="timeseriesDataKeys" md-autocomplete-snap
 						 md-transform-chip="transformTimeseriesDataKeyChip($chip)"
 						 md-require-match="false">
@@ -78,7 +77,6 @@
 			   </md-chips>
 			   <md-chips flex ng-if="widgetType === types.widgetType.latest.value"
                          id="attribute_datakey_chips"
-                         ng-required="true"
                          ng-model="attributeDataKeys" md-autocomplete-snap
                          md-transform-chip="transformAttributeDataKeyChip($chip)"
                          md-require-match="false">
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 7515b61..982685f 100644
--- a/ui/src/app/components/datasource-func.directive.js
+++ b/ui/src/app/components/datasource-func.directive.js
@@ -139,7 +139,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
                 random: tinycolor.random(),
                 clickOutsideToClose: false,
                 hasBackdrop: false,
-                skipHide: true,
+                multiple: true,
                 preserveScope: false,
 
                 mdColorAlphaChannel: true,
@@ -173,7 +173,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: event,
-                skipHide: true,
+                multiple: true,
                 onComplete: function () {
                     var w = angular.element($window);
                     w.triggerHandler('resize');
diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js
index b016f57..97b7bfb 100644
--- a/ui/src/app/components/json-form.directive.js
+++ b/ui/src/app/components/json-form.directive.js
@@ -96,7 +96,7 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
                 random: tinycolor.random(),
                 clickOutsideToClose: false,
                 hasBackdrop: false,
-                skipHide: true,
+                multiple: true,
                 preserveScope: false,
 
                 mdColorAlphaChannel: true,
diff --git a/ui/src/app/components/material-icon-select.directive.js b/ui/src/app/components/material-icon-select.directive.js
index 67caff2..f3efe63 100644
--- a/ui/src/app/components/material-icon-select.directive.js
+++ b/ui/src/app/components/material-icon-select.directive.js
@@ -67,7 +67,7 @@ function MaterialIconSelect($compile, $templateCache, $document, $mdDialog) {
                 templateUrl: materialIconsDialogTemplate,
                 parent: angular.element($document[0].body),
                 locals: {icon: scope.icon},
-                skipHide: true,
+                multiple: true,
                 fullscreen: true,
                 targetEvent: $event
             }).then(function (icon) {
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
index 8110322..9597204 100644
--- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js
+++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
@@ -164,7 +164,7 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
                 .cancel($translate.instant('action.no'))
                 .ok($translate.instant('action.yes'));
 
-            confirm._options.skipHide = true;
+            confirm._options.multiple = true;
             confirm._options.fullscreen = true;
 
             $mdDialog.show(confirm).then(function () {
@@ -212,7 +212,7 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
             locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates,
                 actionSources: availableActionSources, widgetActions: vm.widgetActions,
                 action: angular.copy(action)},
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (action) {
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index caa69d6..fd665fd 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -166,7 +166,7 @@ export default function AddWidgetController($scope, widgetService, entityService
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             targetEvent: event
         }).then(function (singleEntityAlias) {
             vm.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias;
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 6df48df..3f32e1b 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -463,7 +463,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                 }
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (entityAliases) {
@@ -488,7 +488,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                 gridSettings: gridSettings
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (data) {
@@ -510,7 +510,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                 layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts)
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (layouts) {
@@ -531,7 +531,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                 states: states
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (states) {
@@ -873,7 +873,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                 templateUrl: selectTargetLayoutTemplate,
                 parent: angular.element($document[0].body),
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 targetEvent: $event
             }).then(
                 function success(layoutId) {
@@ -941,7 +941,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                         },
                         parent: angular.element($document[0].body),
                         fullscreen: true,
-                        skipHide: true,
+                        multiple: true,
                         targetEvent: event,
                         onComplete: function () {
                             var w = angular.element($window);
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 79653a4..a1bb268 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -74,6 +74,7 @@
                         <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
                     </md-button>
                     <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
+                                           md-theme="tb-dark"
                                            ng-model="vm.currentDashboardId"
                                            dashboards-scope="{{vm.currentDashboardScope}}"
                                            customer-id="vm.currentCustomerId">
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index e6858b4..f8aee69 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -131,7 +131,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
                 },
                 parent: angular.element($document[0].body),
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 targetEvent: event
             }).then(function (singleEntityAlias) {
                 scope.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias;
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
index 5ce8a6c..828bd6f 100644
--- a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
@@ -51,7 +51,7 @@ export default function ManageDashboardLayoutsController($scope, $mdDialog, $doc
                 gridSettings: gridSettings
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (data) {
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
index 98df466..298b2ab 100644
--- a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
@@ -111,7 +111,7 @@ export default function ManageDashboardStatesController($scope, $mdDialog, $filt
             templateUrl: dashboardStateDialogTemplate,
             parent: angular.element($document[0].body),
             locals: {isAdd: isAdd, allStates: vm.allStates, state: angular.copy(state)},
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (state) {
@@ -163,7 +163,7 @@ export default function ManageDashboardStatesController($scope, $mdDialog, $filt
                 .cancel($translate.instant('action.no'))
                 .ok($translate.instant('action.yes'));
 
-            confirm._options.skipHide = true;
+            confirm._options.multiple = true;
             confirm._options.fullscreen = true;
 
             $mdDialog.show(confirm).then(function () {
diff --git a/ui/src/app/entity/alias/entity-aliases.controller.js b/ui/src/app/entity/alias/entity-aliases.controller.js
index 732f10f..49f28c9 100644
--- a/ui/src/app/entity/alias/entity-aliases.controller.js
+++ b/ui/src/app/entity/alias/entity-aliases.controller.js
@@ -126,7 +126,7 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             targetEvent: $event
         }).then(function (alias) {
             if (isAdd) {
@@ -158,7 +158,7 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
                     .ariaLabel($translate.instant('entity.unable-delete-entity-alias-title'))
                     .ok($translate.instant('action.close'))
                     .targetEvent($event);
-                alert._options.skipHide = true;
+                alert._options.multiple = true;
                 alert._options.fullscreen = true;
 
                 $mdDialog.show(alert);
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
index 5346e03..f39593c 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -53,7 +53,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
                     states: states
                 },
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 targetEvent: $event
             }).then(
                 function success(stateId) {
@@ -81,7 +81,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
                 templateUrl: selectTargetLayoutTemplate,
                 parent: angular.element($document[0].body),
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 targetEvent: $event
             }).then(
                 function success(layoutId) {
diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js
index 872042c..d2586c4 100644
--- a/ui/src/app/entity/relation/relation-table.directive.js
+++ b/ui/src/app/entity/relation/relation-table.directive.js
@@ -160,7 +160,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
                       showingCallback: onShowingCallback},
             targetEvent: $event,
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             onShowing: function(scope, element) {
                 onShowingCallback.onShowing(scope, element);
             }
diff --git a/ui/src/app/entity-view/entity-view.directive.js b/ui/src/app/entity-view/entity-view.directive.js
index 25377f4..761930e 100644
--- a/ui/src/app/entity-view/entity-view.directive.js
+++ b/ui/src/app/entity-view/entity-view.directive.js
@@ -98,8 +98,8 @@ export default function EntityViewDirective($q, $compile, $templateCache, $filte
                 if (newDate.getTime() > scope.maxStartTimeMs) {
                     scope.startTimeMs = angular.copy(scope.maxStartTimeMs);
                 }
-                updateMinMaxDates();
             }
+            updateMinMaxDates();
         });
 
         scope.$watch('endTimeMs', function (newDate) {
@@ -107,18 +107,24 @@ export default function EntityViewDirective($q, $compile, $templateCache, $filte
                 if (newDate.getTime() < scope.minEndTimeMs) {
                     scope.endTimeMs = angular.copy(scope.minEndTimeMs);
                 }
-                updateMinMaxDates();
             }
+            updateMinMaxDates();
         });
 
         function updateMinMaxDates() {
-            if (scope.endTimeMs) {
-                scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime()));
-                scope.entityView.endTimeMs = scope.endTimeMs.getTime();
-            }
-            if (scope.startTimeMs) {
-                scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime()));
-                scope.entityView.startTimeMs = scope.startTimeMs.getTime();
+            if (scope.entityView) {
+                if (scope.endTimeMs) {
+                    scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime()));
+                    scope.entityView.endTimeMs = scope.endTimeMs.getTime();
+                } else {
+                    scope.entityView.endTimeMs = 0;
+                }
+                if (scope.startTimeMs) {
+                    scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime()));
+                    scope.entityView.startTimeMs = scope.startTimeMs.getTime();
+                } else {
+                    scope.entityView.startTimeMs = 0;
+                }
             }
         }
 
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index b808fb8..5f84187 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -79,7 +79,7 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: $event,
-                skipHide: true,
+                multiple: true,
                 onShowing: function(scope, element) {
                     onShowingCallback.onShowing(scope, element);
                 }
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
index 18d281c..73c5d93 100644
--- a/ui/src/app/extension/extension-table.directive.js
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -208,7 +208,7 @@ function ExtensionTableController($scope, $filter, $document, $translate, $timeo
             bindToController: true,
             targetEvent: $event,
             fullscreen: true,
-            skipHide: true
+            multiple: true
         }).then(function() {
             reloadExtensions();
         }, function () {
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index d64441f..e08fd24 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -721,7 +721,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                 }
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (updatedEntityAliases) {
@@ -796,7 +796,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                 importFileLabel: importFileLabel
             },
             parent: angular.element($document[0].body),
-            skipHide: true,
+            multiple: true,
             fullscreen: true,
             targetEvent: $event
         }).then(function (importData) {
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 192400a..7c7e870 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -17,7 +17,6 @@ import './home.scss';
 
 import uiRouter from 'angular-ui-router';
 import ngSanitize from 'angular-sanitize';
-import FBAngular from 'angular-fullscreen';
 import 'angular-breadcrumb';
 
 import thingsboardMenu from '../services/menu.service';
@@ -63,7 +62,6 @@ import BreadcrumbIcon from './breadcrumb-icon.filter';
 export default angular.module('thingsboard.home', [
     uiRouter,
     ngSanitize,
-    FBAngular.name,
     'ncy-angular-breadcrumb',
     thingsboardMenu,
     thingsboardHomeLinks,
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 9f326c7..30e1eb1 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -774,6 +774,7 @@
     },
     "entity-view": {
         "entity-view": "Entity View",
+        "entity-view-required": "Entity view is required.",
         "entity-views": "Entity Views",
         "management": "Entity View management",
         "view-entity-views": "View Entity Views",
@@ -1573,7 +1574,7 @@
             "ru_RU": "Russian",
             "es_ES": "Spanish",
             "ja_JA": "Japanese",
-            "TR": "Turkish"
+            "tr_TR": "Turkish"
         }
     }
 }
diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json
index 81cc52e..7b02141 100644
--- a/ui/src/app/locale/locale.constant-es_ES.json
+++ b/ui/src/app/locale/locale.constant-es_ES.json
@@ -1552,7 +1552,7 @@
             "ru_RU": "Ruso",
             "es_ES": "Español",
             "ja_JA": "Japonés",
-            "TR": "Turco"
+            "tr_TR": "Turco"
         }
     }
 }
diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json
index 8b270aa..dd2fa0d 100644
--- a/ui/src/app/locale/locale.constant-fr_FR.json
+++ b/ui/src/app/locale/locale.constant-fr_FR.json
@@ -1010,7 +1010,7 @@
             "ko_KR": "Coréen",
             "ru_RU": "Russe",
             "zh_CN": "Chinois",
-            "TR": "Turc"
+            "tr_TR": "Turc"
         }
     },
     "layout": {
diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json
index 79843ab..49f9b48 100644
--- a/ui/src/app/locale/locale.constant-it_IT.json
+++ b/ui/src/app/locale/locale.constant-it_IT.json
@@ -1442,7 +1442,7 @@
             "ru_RU": "Russo",
             "es_ES": "Spagnolo",
             "ja_JA": "Giapponese",
-            "TR": "Turco"
+            "tr_TR": "Turco"
         }
     }
 }
diff --git a/ui/src/app/locale/locale.constant-ja_JA.json b/ui/src/app/locale/locale.constant-ja_JA.json
index 6c48894..2ab2bcb 100644
--- a/ui/src/app/locale/locale.constant-ja_JA.json
+++ b/ui/src/app/locale/locale.constant-ja_JA.json
@@ -1458,7 +1458,7 @@
 			"ru_RU": "ロシア",
 			"es_ES": "スペイン語",
 			"ja_JA": "日本語",
-			"TR": "トルコ語"
+			"tr_TR": "トルコ語"
 		}
 	}
 }
diff --git a/ui/src/app/locale/locale.constant-ko_KR.json b/ui/src/app/locale/locale.constant-ko_KR.json
index 07e7dd6..62c0054 100644
--- a/ui/src/app/locale/locale.constant-ko_KR.json
+++ b/ui/src/app/locale/locale.constant-ko_KR.json
@@ -1336,7 +1336,7 @@
             "es_ES": "스페인어",
             "it_IT": "이탈리아 사람",
             "ja_JA": "일본어",
-            "TR": "터키어"
+            "tr_TR": "터키어"
         }
     }
 }
diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json
index 893bbcd..eb9996d 100644
--- a/ui/src/app/locale/locale.constant-ru_RU.json
+++ b/ui/src/app/locale/locale.constant-ru_RU.json
@@ -1361,7 +1361,7 @@
             "it_IT": "Итальянский",
             "ru_RU": "Русский",
             "ja_JA": "Японский",
-            "TR": "Турецкий"
+            "tr_TR": "Турецкий"
         }
 
     }
diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json
index c952f9c..bac1706 100644
--- a/ui/src/app/locale/locale.constant-zh_CN.json
+++ b/ui/src/app/locale/locale.constant-zh_CN.json
@@ -1445,7 +1445,7 @@
             "es_ES": "西班牙语",
             "it_IT": "意大利",
             "ja_JA": "日本",
-            "TR": "土耳其"
+            "tr_TR": "土耳其"
         }
     }
 }
diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js
index 81d81c1..d4e6df8 100644
--- a/ui/src/app/rulechain/script/node-script-test.service.js
+++ b/ui/src/app/rulechain/script/node-script-test.service.js
@@ -121,7 +121,7 @@ export default function NodeScriptTest($q, $mdDialog, $document, ruleChainServic
                 onShowingCallback: onShowingCallback
             },
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             targetEvent: $event,
             onComplete: () => {
                 onShowingCallback.onShowed();
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
index e5fb9c3..e7f50cc 100644
--- a/ui/src/app/rulechain/script/node-script-test.tpl.html
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -98,7 +98,7 @@
                                              validate-content="false"
                                              ng-readonly="true"
                                              fill-height="true">
-                            </tb-json-content>generateReport
+                            </tb-json-content>
                         </div>
                     </div>
                 </div>
diff --git a/ui/src/app/user/add-user.controller.js b/ui/src/app/user/add-user.controller.js
index 20ad0be..c81cd67 100644
--- a/ui/src/app/user/add-user.controller.js
+++ b/ui/src/app/user/add-user.controller.js
@@ -100,7 +100,7 @@ export default function AddUserController($scope, $mdDialog, $state, $stateParam
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             targetEvent: $event
         }).then(function () {
             deferred.resolve();
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index 8d7f214..ef7948d 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -186,7 +186,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
-            skipHide: true,
+            multiple: true,
             targetEvent: event
         });
     }
diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js
index 6ae17e0..3d91e9a 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.js
+++ b/ui/src/app/widget/lib/alarms-table-widget.js
@@ -402,7 +402,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
                 parent: angular.element($document[0].body),
                 targetEvent: $event,
                 fullscreen: true,
-                skipHide: true,
+                multiple: true,
                 onShowing: function(scope, element) {
                     onShowingCallback.onShowing(scope, element);
                 }
diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js
index 283a426..274cc97 100644
--- a/ui/src/app/widget/lib/canvas-digital-gauge.js
+++ b/ui/src/app/widget/lib/canvas-digital-gauge.js
@@ -209,8 +209,8 @@ export default class TbCanvasDigitalGauge {
                 }
                 var value = tvPair[1];
                 if(value !== this.gauge.value) {
-                    this.gauge.value = value;
                     this.gauge._value = value;
+                    this.gauge.value = value;
                 } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
                     this.gauge.timestamp = timestamp;
                 }