From 0867030cba748aabe041a91e58a50e87d923a25f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 8 Sep 2023 14:58:33 +0300 Subject: [PATCH 01/39] Add device state node implementation --- .../queue/DefaultTbCoreConsumerService.java | 54 ++- .../state/DefaultDeviceStateService.java | 54 +-- .../service/state/DeviceStateService.java | 2 + .../src/main/resources/thingsboard.yml | 6 +- .../state/DefaultDeviceStateServiceTest.java | 169 +++++++++- common/cluster-api/src/main/proto/queue.proto | 24 ++ .../rule/engine/action/TbDeviceStateNode.java | 178 ++++++++++ .../TbDeviceStateNodeConfiguration.java | 34 ++ .../engine/action/TbDeviceStateNodeTest.java | 313 ++++++++++++++++++ 9 files changed, 796 insertions(+), 38 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index fd5a252cc5..02850e9d8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -256,14 +256,23 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray()); if (actorMsg.isPresent()) { @@ -592,17 +601,54 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> stats = new HashMap<>(); for (DeviceStateData stateData : deviceStates.values()) { Pair tenantDevicesActivity = stats.computeIfAbsent(stateData.getTenantId(), @@ -489,10 +500,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(List.of(deviceIdInfo), 0, 1, false)); } + @Test + public void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() { + // GIVEN + service.deviceStates.put(deviceId, DeviceStateData.builder().build()); + given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)) + .willReturn(TopicPartitionInfo.builder().build()); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId); + + // THEN + assertThat(service.deviceStates).isEmpty(); + then(service).should(never()).fetchDeviceStateDataUsingSeparateRequests(deviceId); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @Test + public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() throws InterruptedException { + // GIVEN + initStateService(10000); + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + + // WHEN + long timeBeforeCall = System.currentTimeMillis(); + service.onDeviceInactivity(tenantId, deviceId); + long timeAfterCall = System.currentTimeMillis(); + + // THEN + var inactivityTimeCaptor = ArgumentCaptor.forClass(Long.class); + then(telemetrySubscriptionService).should().saveAttrAndNotify( + any(), eq(deviceId), any(), eq(INACTIVITY_ALARM_TIME), inactivityTimeCaptor.capture(), any() + ); + var actualInactivityTime = inactivityTimeCaptor.getValue(); + assertThat(actualInactivityTime).isGreaterThanOrEqualTo(timeBeforeCall); + assertThat(actualInactivityTime).isLessThanOrEqualTo(timeAfterCall); + + then(telemetrySubscriptionService).should().saveAttrAndNotify( + any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(false), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + + var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + var actualNotification = notificationCaptor.getValue(); + assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); + assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); + assertThat(actualNotification.isActive()).isFalse(); + } + + @Test + public void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() { + // GIVEN + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); + + // WHEN + service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); + + // THEN + then(telemetrySubscriptionService).should().saveAttrAndNotify( + any(), eq(deviceId), any(), eq(INACTIVITY_ALARM_TIME), anyLong(), any() + ); + then(telemetrySubscriptionService).should().saveAttrAndNotify( + any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(false), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + + var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + var actualNotification = notificationCaptor.getValue(); + assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); + assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); + assertThat(actualNotification.isActive()).isFalse(); + } + @Test public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { service.deviceStates.put(deviceId, deviceStateDataMock); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); - assertThat(deviceStateData, is(deviceStateDataMock)); - Mockito.verify(service, never()).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + assertThat(deviceStateData).isEqualTo(deviceStateDataMock); + Mockito.verify(service, never()).fetchDeviceStateDataUsingSeparateRequests(deviceId); } @Test public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { service.deviceStates.clear(); - willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); - assertThat(deviceStateData, is(deviceStateDataMock)); - Mockito.verify(service, times(1)).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + assertThat(deviceStateData).isEqualTo(deviceStateDataMock); + Mockito.verify(service, times(1)).fetchDeviceStateDataUsingSeparateRequests(deviceId); } @Test @@ -341,4 +459,37 @@ public class DefaultDeviceStateServiceTest { Mockito.verify(telemetrySubscriptionService, Mockito.times(1)).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.eq(isActive), Mockito.any()); } -} \ No newline at end of file + @Test + public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { + var deviceStateData = DeviceStateData.builder().build(); + var getOrFetchInvocationCounter = new AtomicInteger(); + + doAnswer(invocation -> { + getOrFetchInvocationCounter.incrementAndGet(); + Thread.sleep(100); + return deviceStateData; + }).when(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); + + int numberOfThreads = 10; + var allThreadsReadyLatch = new CountDownLatch(numberOfThreads); + var executor = Executors.newFixedThreadPool(numberOfThreads); + + for (int i = 0; i < numberOfThreads; i++) { + executor.submit(() -> { + allThreadsReadyLatch.countDown(); + try { + allThreadsReadyLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + service.getOrFetchDeviceStateData(deviceId); + }); + } + + executor.shutdown(); + await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated); + + assertThat(getOrFetchInvocationCounter.get()).isEqualTo(1); + } + +} diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 60dfcae1de..5260a4fa26 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -480,6 +480,13 @@ message GetOtaPackageResponseMsg { string fileName = 8; } +message DeviceConnectProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; +} + message DeviceActivityProto { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; @@ -488,6 +495,20 @@ message DeviceActivityProto { int64 lastActivityTime = 5; } +message DeviceDisconnectProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; +} + +message DeviceInactivityProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; +} + //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -977,6 +998,9 @@ message ToCoreMsg { NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7; LifecycleEventProto lifecycleEventMsg = 8; ErrorEventProto errorEventMsg = 9; + DeviceConnectProto deviceConnectMsg = 10; + DeviceDisconnectProto deviceDisconnectMsg = 11; + DeviceInactivityProto deviceInactivityMsg = 12; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java new file mode 100644 index 0000000000..7003f78a00 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -0,0 +1,178 @@ +/** + * Copyright © 2016-2023 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.rule.engine.action; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; + +import java.util.Map; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "device state", + nodeDescription = "Triggers device connectivity events", + nodeDetails = "If incoming message originator is a device," + + " registers configured event for that device in the Device State Service," + + " which sends appropriate message to the Rule Engine. " + + "Incoming message is forwarded using the Success chain," + + " unless an unexpected error occurs during message processing" + + " then incoming message is forwarded using the Failure chain." + + "
" + + "Supported device connectivity events are:" + + "
    " + + "
  • Connect event
  • " + + "
  • Disconnect event
  • " + + "
  • Activity event
  • " + + "
  • Inactivity event
  • " + + "
" + + "This node is particularly useful when device isn't using transports to receive data," + + " such as when fetching data from external API or computing new data within the rule chain.", + configClazz = TbDeviceStateNodeConfiguration.class, + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeDeviceStateConfig" +) +public class TbDeviceStateNode implements TbNode { + + private static final TbQueueCallback EMPTY_CALLBACK = new TbQueueCallback() { + + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + } + + @Override + public void onFailure(Throwable t) { + } + + }; + + private final Map SUPPORTED_EVENTS = Map.of( + TbMsgType.CONNECT_EVENT, this::sendDeviceConnectMsg, + TbMsgType.ACTIVITY_EVENT, this::sendDeviceActivityMsg, + TbMsgType.DISCONNECT_EVENT, this::sendDeviceDisconnectMsg, + TbMsgType.INACTIVITY_EVENT, this::sendDeviceInactivityMsg + ); + + private interface ConnectivityEvent { + + void sendEvent(TbContext ctx, TbMsg msg); + + } + + private TbMsgType event; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + TbMsgType event = TbNodeUtils.convert(configuration, TbDeviceStateNodeConfiguration.class).getEvent(); + if (event == null) { + throw new TbNodeException("Event cannot be null!", true); + } + if (!SUPPORTED_EVENTS.containsKey(event)) { + throw new TbNodeException("Unsupported event: " + event, true); + } + this.event = event; + + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + var originator = msg.getOriginator(); + if (!ctx.isLocalEntity(originator)) { + log.warn("[{}][device-state-node] Received message from non-local entity [{}]!", ctx.getSelfId(), originator); + return; + } + if (!EntityType.DEVICE.equals(originator.getEntityType())) { + ctx.tellSuccess(msg); + return; + } + SUPPORTED_EVENTS.get(event).sendEvent(ctx, msg); + ctx.tellSuccess(msg); + } + + private void sendDeviceConnectMsg(TbContext ctx, TbMsg msg) { + var tenantUuid = ctx.getTenantId().getId(); + var deviceUuid = msg.getOriginator().getId(); + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + } + + private void sendDeviceActivityMsg(TbContext ctx, TbMsg msg) { + var tenantUuid = ctx.getTenantId().getId(); + var deviceUuid = msg.getOriginator().getId(); + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastActivityTime(System.currentTimeMillis()) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + } + + private void sendDeviceDisconnectMsg(TbContext ctx, TbMsg msg) { + var tenantUuid = ctx.getTenantId().getId(); + var deviceUuid = msg.getOriginator().getId(); + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + } + + private void sendDeviceInactivityMsg(TbContext ctx, TbMsg msg) { + var tenantUuid = ctx.getTenantId().getId(); + var deviceUuid = msg.getOriginator().getId(); + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java new file mode 100644 index 0000000000..c89fcb076f --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 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.rule.engine.action; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.msg.TbMsgType; + +@Data +public class TbDeviceStateNodeConfiguration implements NodeConfiguration { + + private TbMsgType event; + + @Override + public TbDeviceStateNodeConfiguration defaultConfiguration() { + var config = new TbDeviceStateNodeConfiguration(); + config.setEvent(TbMsgType.ACTIVITY_EVENT); + return config; + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java new file mode 100644 index 0000000000..1aaabce61d --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -0,0 +1,313 @@ +/** + * Copyright © 2016-2023 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.rule.engine.action; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +public class TbDeviceStateNodeTest { + + private static TenantId DUMMY_TENANT_ID; + private static TbMsg DUMMY_MSG; + private static DeviceId DUMMY_MSG_ORIGINATOR; + @Mock + private TbContext ctxMock; + @Mock + private TbClusterService tbClusterServiceMock; + private TbDeviceStateNode node; + private TbDeviceStateNodeConfiguration config; + + @BeforeAll + public static void init() { + DUMMY_TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + + var device = new Device(); + device.setTenantId(DUMMY_TENANT_ID); + device.setId(new DeviceId(UUID.randomUUID())); + device.setName("My humidity sensor"); + device.setType("Humidity sensor"); + device.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); + var metaData = new TbMsgMetaData(); + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + metaData.putValue("ts", String.valueOf(System.currentTimeMillis())); + var data = JacksonUtil.newObjectNode(); + data.put("humidity", 58.3); + DUMMY_MSG = TbMsg.newMsg( + TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), metaData, JacksonUtil.toString(data) + ); + DUMMY_MSG_ORIGINATOR = device.getId(); + } + + @BeforeEach + public void setUp() { + node = new TbDeviceStateNode(); + config = new TbDeviceStateNodeConfiguration(); + } + + @Test + public void givenDefaultConfiguration_whenInvoked_thenCorrectValuesAreSet() { + // GIVEN-WHEN + config = config.defaultConfiguration(); + + // THEN + assertThat(config.getEvent()).isEqualTo(TbMsgType.ACTIVITY_EVENT); + } + + @Test + public void givenNullEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + config.setEvent(null); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + // WHEN-THEN + assertThatThrownBy(() -> node.init(ctxMock, nodeConfig)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Event cannot be null!") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @Test + public void givenUnsupportedEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + var unsupportedEvent = TbMsgType.TO_SERVER_RPC_REQUEST; + config.setEvent(unsupportedEvent); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + // WHEN-THEN + assertThatThrownBy(() -> node.init(ctxMock, nodeConfig)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Unsupported event: " + unsupportedEvent) + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @Test + public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { + // GIVEN + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, new AssetId(UUID.randomUUID()), new TbMsgMetaData(), "{}"); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should(only()).tellSuccess(msg); + } + + @Test + public void givenNonLocalOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { + // GIVEN + given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(false); + + // WHEN + node.onMsg(ctxMock, DUMMY_MSG); + + // THEN + then(ctxMock).should(times(1)).isLocalEntity(DUMMY_MSG_ORIGINATOR); + then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenConnectEventInConfig_whenOnMsg_thenOnDeviceConnectCalledAndTellsSuccess() throws TbNodeException { + // GIVEN + config.setEvent(TbMsgType.CONNECT_EVENT); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); + given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); + given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); + + // WHEN + node.onMsg(ctxMock, DUMMY_MSG); + + // THEN + var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); + then(tbClusterServiceMock).should(times(1)) + .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); + + TransportProtos.DeviceConnectProto expectedDeviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) + .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) + .build(); + TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(expectedDeviceConnectMsg) + .build(); + assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); + + then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenActivityEventInConfig_whenOnMsg_thenOnDeviceActivityCalledWithCorrectTimeAndTellsSuccess() + throws TbNodeException { + // GIVEN + config.setEvent(TbMsgType.ACTIVITY_EVENT); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); + given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); + given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); + + + // WHEN + long timeBeforeCall = System.currentTimeMillis(); + node.onMsg(ctxMock, DUMMY_MSG); + long timeAfterCall = System.currentTimeMillis(); + + // THEN + var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); + then(tbClusterServiceMock).should(times(1)) + .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); + + TransportProtos.ToCoreMsg actualToCoreMsg = protoCaptor.getValue(); + long actualLastActivityTime = actualToCoreMsg.getDeviceActivityMsg().getLastActivityTime(); + + assertThat(actualLastActivityTime).isGreaterThanOrEqualTo(timeBeforeCall); + assertThat(actualLastActivityTime).isLessThanOrEqualTo(timeAfterCall); + + TransportProtos.DeviceActivityProto updatedActivityMsg = actualToCoreMsg.getDeviceActivityMsg().toBuilder() + .setLastActivityTime(123L) + .build(); + TransportProtos.ToCoreMsg updatedToCoreMsg = actualToCoreMsg.toBuilder() + .setDeviceActivityMsg(updatedActivityMsg) + .build(); + + TransportProtos.DeviceActivityProto expectedDeviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) + .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) + .setLastActivityTime(123L) + .build(); + TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(expectedDeviceActivityMsg) + .build(); + + assertThat(updatedToCoreMsg).isEqualTo(expectedToCoreMsg); + + then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenInactivityEventInConfig_whenOnMsg_thenOnDeviceInactivityCalledAndTellsSuccess() + throws TbNodeException { + // GIVEN + config.setEvent(TbMsgType.INACTIVITY_EVENT); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); + given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); + given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); + + // WHEN + node.onMsg(ctxMock, DUMMY_MSG); + + // THEN + var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); + then(tbClusterServiceMock).should(times(1)) + .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); + + TransportProtos.DeviceInactivityProto expectedDeviceInactivityMsg = + TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) + .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) + .build(); + TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(expectedDeviceInactivityMsg) + .build(); + assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); + + then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDisconnectEventInConfig_whenOnMsg_thenOnDeviceDisconnectCalledAndTellsSuccess() + throws TbNodeException { + // GIVEN + config.setEvent(TbMsgType.DISCONNECT_EVENT); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); + given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); + given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); + + // WHEN + node.onMsg(ctxMock, DUMMY_MSG); + + // THEN + var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); + then(tbClusterServiceMock).should(times(1)) + .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); + + TransportProtos.DeviceDisconnectProto expectedDeviceDisconnectMsg = + TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) + .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) + .build(); + TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(expectedDeviceDisconnectMsg) + .build(); + assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); + + then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + +} From 0c5e7a933cde78a877c1adc84ba126a41dec4bdf Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 8 Sep 2023 18:30:13 +0300 Subject: [PATCH 02/39] Fix test --- .../engine/action/TbDeviceStateNodeTest.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 1aaabce61d..123dffd0ea 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -41,11 +41,11 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.only; import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) @@ -128,13 +128,17 @@ public class TbDeviceStateNodeTest { @Test public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { // GIVEN - var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, new AssetId(UUID.randomUUID()), new TbMsgMetaData(), "{}"); + var originator = new AssetId(UUID.randomUUID()); + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, originator, new TbMsgMetaData(), "{}"); + given(ctxMock.isLocalEntity(originator)).willReturn(true); // WHEN node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should(only()).tellSuccess(msg); + then(ctxMock).should(times(1)).isLocalEntity(originator); + then(ctxMock).should(times(1)).tellSuccess(msg); + then(ctxMock).shouldHaveNoMoreInteractions(); } @Test @@ -147,16 +151,20 @@ public class TbDeviceStateNodeTest { // THEN then(ctxMock).should(times(1)).isLocalEntity(DUMMY_MSG_ORIGINATOR); - then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); + then(ctxMock).should().getSelfId(); then(ctxMock).shouldHaveNoMoreInteractions(); } @Test - public void givenConnectEventInConfig_whenOnMsg_thenOnDeviceConnectCalledAndTellsSuccess() throws TbNodeException { + public void givenConnectEventInConfig_whenOnMsg_thenOnDeviceConnectCalledAndTellsSuccess() { // GIVEN config.setEvent(TbMsgType.CONNECT_EVENT); var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctxMock, nodeConfig); + try { + node.init(ctxMock, nodeConfig); + } catch (TbNodeException e) { + fail("Node failed to initialize!", e); + } given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); From 51aae476d35f73537a139d019af397a4240e83c5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 12 Sep 2023 13:40:58 +0300 Subject: [PATCH 03/39] Move telling success into queue callback --- .../rule/engine/action/TbDeviceStateNode.java | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 7003f78a00..8adafe8d62 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -59,18 +59,6 @@ import java.util.Map; ) public class TbDeviceStateNode implements TbNode { - private static final TbQueueCallback EMPTY_CALLBACK = new TbQueueCallback() { - - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - } - - @Override - public void onFailure(Throwable t) { - } - - }; - private final Map SUPPORTED_EVENTS = Map.of( TbMsgType.CONNECT_EVENT, this::sendDeviceConnectMsg, TbMsgType.ACTIVITY_EVENT, this::sendDeviceActivityMsg, @@ -96,7 +84,6 @@ public class TbDeviceStateNode implements TbNode { throw new TbNodeException("Unsupported event: " + event, true); } this.event = event; - } @Override @@ -126,7 +113,9 @@ public class TbDeviceStateNode implements TbNode { var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceConnectMsg(deviceConnectMsg) .build(); - ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + ctx.getClusterService().pushMsgToCore( + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ); } private void sendDeviceActivityMsg(TbContext ctx, TbMsg msg) { @@ -142,7 +131,9 @@ public class TbDeviceStateNode implements TbNode { var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceActivityMsg(deviceActivityMsg) .build(); - ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + ctx.getClusterService().pushMsgToCore( + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ); } private void sendDeviceDisconnectMsg(TbContext ctx, TbMsg msg) { @@ -157,7 +148,9 @@ public class TbDeviceStateNode implements TbNode { var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceDisconnectMsg(deviceDisconnectMsg) .build(); - ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + ctx.getClusterService().pushMsgToCore( + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ); } private void sendDeviceInactivityMsg(TbContext ctx, TbMsg msg) { @@ -172,7 +165,23 @@ public class TbDeviceStateNode implements TbNode { var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceInactivityMsg(deviceInactivityMsg) .build(); - ctx.getClusterService().pushMsgToCore(ctx.getTenantId(), msg.getOriginator(), toCoreMsg, EMPTY_CALLBACK); + ctx.getClusterService().pushMsgToCore( + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ); + } + + private TbQueueCallback getMsgProcessedCallback(TbContext ctx, TbMsg msg) { + return new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + ctx.tellSuccess(msg); + } + + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + }; } } From 6b5b654546fb5b49446c0fccc1eb53eff36bff91 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 12 Sep 2023 13:42:18 +0300 Subject: [PATCH 04/39] Fix grammar --- .../service/queue/DefaultTbCoreConsumerService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 02850e9d8c..8f3b0efc5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -608,7 +608,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService Date: Tue, 19 Sep 2023 14:02:23 +0300 Subject: [PATCH 05/39] Add review fixes --- .../device/DeviceActorMessageProcessor.java | 4 +- .../actors/ruleChain/DefaultTbContext.java | 44 ++-- .../queue/DefaultTbCoreConsumerService.java | 14 +- .../state/DefaultDeviceStateService.java | 45 ++-- .../service/state/DeviceStateService.java | 6 +- .../state/DefaultDeviceStateServiceTest.java | 97 +++---- common/cluster-api/src/main/proto/queue.proto | 3 + .../queue/common/SimpleTbQueueCallback.java | 47 ++++ .../rule/engine/action/TbDeviceStateNode.java | 78 +++--- .../engine/action/TbDeviceStateNodeTest.java | 244 ++++++------------ 10 files changed, 278 insertions(+), 304 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java 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 b838f0b86d..55262cbf63 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 @@ -490,11 +490,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private void reportSessionOpen() { - systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId); + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId, System.currentTimeMillis()); } private void reportSessionClose() { - systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis()); } private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { 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 ca1d174322..ed07456ed6 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 @@ -102,8 +102,7 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; @@ -209,7 +208,13 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain"); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(onSuccess, onFailure)); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(onSuccess, t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override @@ -295,7 +300,13 @@ class DefaultTbContext implements TbContext { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage)); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(onSuccess, onFailure)); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(onSuccess, t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override @@ -923,29 +934,4 @@ class DefaultTbContext implements TbContext { return failureMessage; } - private class SimpleTbQueueCallback implements TbQueueCallback { - private final Runnable onSuccess; - private final Consumer onFailure; - - public SimpleTbQueueCallback(Runnable onSuccess, Consumer onFailure) { - this.onSuccess = onSuccess; - this.onFailure = onFailure; - } - - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - if (onSuccess != null) { - onSuccess.run(); - } - } - - @Override - public void onFailure(Throwable t) { - if (onFailure != null) { - onFailure.accept(t); - } else { - log.debug("[{}] Failed to put item into queue", nodeCtx.getTenantId(), t); - } - } - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 8f3b0efc5b..7fb60f5da0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -605,10 +605,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) { updateActivityState(deviceId, stateData, lastReportedActivity); @@ -259,15 +259,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService kvEntries, String attributeName, long defaultValue) { if (kvEntries != null) { for (KvEntry entry : kvEntries) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java index b724b6e431..82c91d9baf 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java @@ -27,13 +27,13 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; */ public interface DeviceStateService extends ApplicationListener { - void onDeviceConnect(TenantId tenantId, DeviceId deviceId); + void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long lastConnectTime); void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime); - void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId); + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long lastDisconnectTime); - void onDeviceInactivity(TenantId tenantId, DeviceId deviceId); + void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long lastInactivityTime); void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 435fdbc523..6f99f3cfce 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -25,6 +25,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,11 +55,12 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -128,7 +130,7 @@ public class DefaultDeviceStateServiceTest { .willReturn(TopicPartitionInfo.builder().build()); // WHEN - service.onDeviceInactivity(tenantId, deviceId); + service.onDeviceInactivity(tenantId, deviceId, System.currentTimeMillis()); // THEN assertThat(service.deviceStates).isEmpty(); @@ -139,9 +141,13 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() throws InterruptedException { + public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { // GIVEN - initStateService(10000); + try { + initStateService(10000); + } catch (InterruptedException e) { + fail("Device state service failed to initialize!"); + } var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) .deviceId(deviceId) @@ -150,33 +156,30 @@ public class DefaultDeviceStateServiceTest { .build(); service.deviceStates.put(deviceId, deviceStateData); + long lastInactivityTime = System.currentTimeMillis(); // WHEN - long timeBeforeCall = System.currentTimeMillis(); - service.onDeviceInactivity(tenantId, deviceId); - long timeAfterCall = System.currentTimeMillis(); + service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime); // THEN - var inactivityTimeCaptor = ArgumentCaptor.forClass(Long.class); - then(telemetrySubscriptionService).should().saveAttrAndNotify( - any(), eq(deviceId), any(), eq(INACTIVITY_ALARM_TIME), inactivityTimeCaptor.capture(), any() + then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(INACTIVITY_ALARM_TIME), eq(lastInactivityTime), any() ); - var actualInactivityTime = inactivityTimeCaptor.getValue(); - assertThat(actualInactivityTime).isGreaterThanOrEqualTo(timeBeforeCall); - assertThat(actualInactivityTime).isLessThanOrEqualTo(timeAfterCall); - - then(telemetrySubscriptionService).should().saveAttrAndNotify( - any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(false), any() + then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(ACTIVITY_STATE), eq(false), any() ); var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + then(clusterService).should(times(1)) + .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); var actualMsg = msgCaptor.getValue(); assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); - then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + then(notificationRuleProcessor).should(times(1)).process(notificationCaptor.capture()); var actualNotification = notificationCaptor.getValue(); assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); @@ -199,21 +202,24 @@ public class DefaultDeviceStateServiceTest { service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); // THEN - then(telemetrySubscriptionService).should().saveAttrAndNotify( - any(), eq(deviceId), any(), eq(INACTIVITY_ALARM_TIME), anyLong(), any() + then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(INACTIVITY_ALARM_TIME), anyLong(), any() ); - then(telemetrySubscriptionService).should().saveAttrAndNotify( - any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(false), any() + then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(ACTIVITY_STATE), eq(false), any() ); var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + then(clusterService).should(times(1)) + .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); var actualMsg = msgCaptor.getValue(); assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); - then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + then(notificationRuleProcessor).should(times(1)).process(notificationCaptor.capture()); var actualNotification = notificationCaptor.getValue(); assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); @@ -461,35 +467,38 @@ public class DefaultDeviceStateServiceTest { @Test public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { - var deviceStateData = DeviceStateData.builder().build(); - var getOrFetchInvocationCounter = new AtomicInteger(); - doAnswer(invocation -> { - getOrFetchInvocationCounter.incrementAndGet(); Thread.sleep(100); - return deviceStateData; + return deviceStateDataMock; }).when(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); int numberOfThreads = 10; var allThreadsReadyLatch = new CountDownLatch(numberOfThreads); - var executor = Executors.newFixedThreadPool(numberOfThreads); - for (int i = 0; i < numberOfThreads; i++) { - executor.submit(() -> { - allThreadsReadyLatch.countDown(); - try { - allThreadsReadyLatch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - service.getOrFetchDeviceStateData(deviceId); - }); + ExecutorService executor = null; + try { + executor = Executors.newFixedThreadPool(numberOfThreads); + for (int i = 0; i < numberOfThreads; i++) { + executor.submit(() -> { + allThreadsReadyLatch.countDown(); + try { + allThreadsReadyLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + service.getOrFetchDeviceStateData(deviceId); + }); + } + + executor.shutdown(); + await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated); + } finally { + if (executor != null) { + executor.shutdownNow(); + } } - executor.shutdown(); - await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated); - - assertThat(getOrFetchInvocationCounter.get()).isEqualTo(1); + then(service).should(times(1)).fetchDeviceStateDataUsingSeparateRequests(deviceId); } } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 5260a4fa26..ef6a644c7f 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -485,6 +485,7 @@ message DeviceConnectProto { int64 tenantIdLSB = 2; int64 deviceIdMSB = 3; int64 deviceIdLSB = 4; + int64 lastConnectTime = 5; } message DeviceActivityProto { @@ -500,6 +501,7 @@ message DeviceDisconnectProto { int64 tenantIdLSB = 2; int64 deviceIdMSB = 3; int64 deviceIdLSB = 4; + int64 lastDisconnectTime = 5; } message DeviceInactivityProto { @@ -507,6 +509,7 @@ message DeviceInactivityProto { int64 tenantIdLSB = 2; int64 deviceIdMSB = 3; int64 deviceIdLSB = 4; + int64 lastInactivityTime = 5; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java new file mode 100644 index 0000000000..805511a603 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2023 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.queue.common; + +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; + +import java.util.function.Consumer; + +public class SimpleTbQueueCallback implements TbQueueCallback { + + private final Runnable onSuccess; + private final Consumer onFailure; + + public SimpleTbQueueCallback(Runnable onSuccess, Consumer onFailure) { + this.onSuccess = onSuccess; + this.onFailure = onFailure; + } + + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + if (onSuccess != null) { + onSuccess.run(); + } + } + + @Override + public void onFailure(Throwable t) { + if (onFailure != null) { + onFailure.accept(t); + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 8adafe8d62..37ecf34cdd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; -import java.util.Map; +import java.util.Set; @Slf4j @RuleNode( @@ -39,8 +39,9 @@ import java.util.Map; nodeDescription = "Triggers device connectivity events", nodeDetails = "If incoming message originator is a device," + " registers configured event for that device in the Device State Service," + - " which sends appropriate message to the Rule Engine. " + - "Incoming message is forwarded using the Success chain," + + " which sends appropriate message to the Rule Engine." + + " If metadata ts property is present, it will be used as event timestamp." + + " Incoming message is forwarded using the Success chain," + " unless an unexpected error occurs during message processing" + " then incoming message is forwarded using the Failure chain." + "
" + @@ -59,19 +60,10 @@ import java.util.Map; ) public class TbDeviceStateNode implements TbNode { - private final Map SUPPORTED_EVENTS = Map.of( - TbMsgType.CONNECT_EVENT, this::sendDeviceConnectMsg, - TbMsgType.ACTIVITY_EVENT, this::sendDeviceActivityMsg, - TbMsgType.DISCONNECT_EVENT, this::sendDeviceDisconnectMsg, - TbMsgType.INACTIVITY_EVENT, this::sendDeviceInactivityMsg + private static final Set SUPPORTED_EVENTS = Set.of( + TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); - private interface ConnectivityEvent { - - void sendEvent(TbContext ctx, TbMsg msg); - - } - private TbMsgType event; @Override @@ -80,7 +72,7 @@ public class TbDeviceStateNode implements TbNode { if (event == null) { throw new TbNodeException("Event cannot be null!", true); } - if (!SUPPORTED_EVENTS.containsKey(event)) { + if (!SUPPORTED_EVENTS.contains(event)) { throw new TbNodeException("Unsupported event: " + event, true); } this.event = event; @@ -90,15 +82,36 @@ public class TbDeviceStateNode implements TbNode { public void onMsg(TbContext ctx, TbMsg msg) { var originator = msg.getOriginator(); if (!ctx.isLocalEntity(originator)) { - log.warn("[{}][device-state-node] Received message from non-local entity [{}]!", ctx.getSelfId(), originator); + log.warn("[{}] Node [{}] received message from non-local entity [{}]!", + ctx.getTenantId().getId(), ctx.getSelfId().getId(), originator.getId()); + ctx.ack(msg); return; } if (!EntityType.DEVICE.equals(originator.getEntityType())) { ctx.tellSuccess(msg); return; } - SUPPORTED_EVENTS.get(event).sendEvent(ctx, msg); - ctx.tellSuccess(msg); + switch (event) { + case CONNECT_EVENT: { + sendDeviceConnectMsg(ctx, msg); + break; + } + case ACTIVITY_EVENT: { + sendDeviceActivityMsg(ctx, msg); + break; + } + case DISCONNECT_EVENT: { + sendDeviceDisconnectMsg(ctx, msg); + break; + } + case INACTIVITY_EVENT: { + sendDeviceInactivityMsg(ctx, msg); + break; + } + default: { + ctx.tellFailure(msg, new IllegalStateException("Configured event [" + event + "] is not supported!")); + } + } } private void sendDeviceConnectMsg(TbContext ctx, TbMsg msg) { @@ -109,12 +122,13 @@ public class TbDeviceStateNode implements TbNode { .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastConnectTime(msg.getMetaDataTs()) .build(); var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceConnectMsg(deviceConnectMsg) .build(); ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) ); } @@ -126,13 +140,13 @@ public class TbDeviceStateNode implements TbNode { .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastActivityTime(System.currentTimeMillis()) + .setLastActivityTime(msg.getMetaDataTs()) .build(); var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceActivityMsg(deviceActivityMsg) .build(); ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) ); } @@ -144,12 +158,13 @@ public class TbDeviceStateNode implements TbNode { .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastDisconnectTime(msg.getMetaDataTs()) .build(); var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceDisconnectMsg(deviceDisconnectMsg) .build(); ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) ); } @@ -161,27 +176,18 @@ public class TbDeviceStateNode implements TbNode { .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastInactivityTime(msg.getMetaDataTs()) .build(); var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() .setDeviceInactivityMsg(deviceInactivityMsg) .build(); ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgProcessedCallback(ctx, msg) + ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) ); } - private TbQueueCallback getMsgProcessedCallback(TbContext ctx, TbMsg msg) { - return new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - ctx.tellSuccess(msg); - } - - @Override - public void onFailure(Throwable t) { - ctx.tellFailure(msg, t); - } - }; + private TbQueueCallback getMsgEnqueuedCallback(TbContext ctx, TbMsg msg) { + return new SimpleTbQueueCallback(() -> ctx.tellSuccess(msg), t -> ctx.tellFailure(msg, t)); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 123dffd0ea..176136244d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.rule.engine.action; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -31,18 +33,20 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; @@ -51,9 +55,6 @@ import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) public class TbDeviceStateNodeTest { - private static TenantId DUMMY_TENANT_ID; - private static TbMsg DUMMY_MSG; - private static DeviceId DUMMY_MSG_ORIGINATOR; @Mock private TbContext ctxMock; @Mock @@ -61,26 +62,26 @@ public class TbDeviceStateNodeTest { private TbDeviceStateNode node; private TbDeviceStateNodeConfiguration config; - @BeforeAll - public static void init() { - DUMMY_TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final long METADATA_TS = System.currentTimeMillis(); + private TbMsg msg; + @BeforeEach + public void setup() { var device = new Device(); - device.setTenantId(DUMMY_TENANT_ID); - device.setId(new DeviceId(UUID.randomUUID())); + device.setTenantId(TENANT_ID); + device.setId(DEVICE_ID); device.setName("My humidity sensor"); device.setType("Humidity sensor"); device.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); var metaData = new TbMsgMetaData(); metaData.putValue("deviceName", device.getName()); metaData.putValue("deviceType", device.getType()); - metaData.putValue("ts", String.valueOf(System.currentTimeMillis())); + metaData.putValue("ts", String.valueOf(METADATA_TS)); var data = JacksonUtil.newObjectNode(); data.put("humidity", 58.3); - DUMMY_MSG = TbMsg.newMsg( - TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), metaData, JacksonUtil.toString(data) - ); - DUMMY_MSG_ORIGINATOR = device.getId(); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), metaData, JacksonUtil.toString(data)); } @BeforeEach @@ -128,15 +129,15 @@ public class TbDeviceStateNodeTest { @Test public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { // GIVEN - var originator = new AssetId(UUID.randomUUID()); - var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, originator, new TbMsgMetaData(), "{}"); - given(ctxMock.isLocalEntity(originator)).willReturn(true); + var asset = new AssetId(UUID.randomUUID()); + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, asset, new TbMsgMetaData(), "{}"); + given(ctxMock.isLocalEntity(asset)).willReturn(true); // WHEN node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should(times(1)).isLocalEntity(originator); + then(ctxMock).should(times(1)).isLocalEntity(asset); then(ctxMock).should(times(1)).tellSuccess(msg); then(ctxMock).shouldHaveNoMoreInteractions(); } @@ -144,178 +145,97 @@ public class TbDeviceStateNodeTest { @Test public void givenNonLocalOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { // GIVEN - given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(false); + given(ctxMock.isLocalEntity(msg.getOriginator())).willReturn(false); + given(ctxMock.getSelfId()).willReturn(new RuleNodeId(UUID.randomUUID())); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); // WHEN - node.onMsg(ctxMock, DUMMY_MSG); + node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should(times(1)).isLocalEntity(DUMMY_MSG_ORIGINATOR); + then(ctxMock).should(times(1)).isLocalEntity(DEVICE_ID); + then(ctxMock).should().getTenantId(); then(ctxMock).should().getSelfId(); + then(ctxMock).should(times(1)).ack(msg); then(ctxMock).shouldHaveNoMoreInteractions(); } - @Test - public void givenConnectEventInConfig_whenOnMsg_thenOnDeviceConnectCalledAndTellsSuccess() { + @ParameterizedTest + @MethodSource("provideSupportedEventsAndExpectedMessages") + public void givenSupportedEvent_whenOnMsg_thenCorrectMsgIsSent( + TbMsgType event, TransportProtos.ToCoreMsg expectedToCoreMsg + ) { // GIVEN - config.setEvent(TbMsgType.CONNECT_EVENT); + config.setEvent(event); var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); try { node.init(ctxMock, nodeConfig); } catch (TbNodeException e) { fail("Node failed to initialize!", e); } - given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); - given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); + given(ctxMock.isLocalEntity(msg.getOriginator())).willReturn(true); // WHEN - node.onMsg(ctxMock, DUMMY_MSG); + node.onMsg(ctxMock, msg); // THEN var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); + var callbackCaptor = ArgumentCaptor.forClass(TbQueueCallback.class); then(tbClusterServiceMock).should(times(1)) - .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); + .pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), protoCaptor.capture(), callbackCaptor.capture()); + + TbQueueCallback actualCallback = callbackCaptor.getValue(); + + actualCallback.onSuccess(null); + then(ctxMock).should(times(1)).tellSuccess(msg); + + var throwable = new Throwable(); + actualCallback.onFailure(throwable); + then(ctxMock).should(times(1)).tellFailure(msg, throwable); - TransportProtos.DeviceConnectProto expectedDeviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) - .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) - .build(); - TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceConnectMsg(expectedDeviceConnectMsg) - .build(); assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); - then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); then(ctxMock).shouldHaveNoMoreInteractions(); } - @Test - public void givenActivityEventInConfig_whenOnMsg_thenOnDeviceActivityCalledWithCorrectTimeAndTellsSuccess() - throws TbNodeException { - // GIVEN - config.setEvent(TbMsgType.ACTIVITY_EVENT); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctxMock, nodeConfig); - given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); - given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); - given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); - - - // WHEN - long timeBeforeCall = System.currentTimeMillis(); - node.onMsg(ctxMock, DUMMY_MSG); - long timeAfterCall = System.currentTimeMillis(); - - // THEN - var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); - then(tbClusterServiceMock).should(times(1)) - .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); - - TransportProtos.ToCoreMsg actualToCoreMsg = protoCaptor.getValue(); - long actualLastActivityTime = actualToCoreMsg.getDeviceActivityMsg().getLastActivityTime(); - - assertThat(actualLastActivityTime).isGreaterThanOrEqualTo(timeBeforeCall); - assertThat(actualLastActivityTime).isLessThanOrEqualTo(timeAfterCall); - - TransportProtos.DeviceActivityProto updatedActivityMsg = actualToCoreMsg.getDeviceActivityMsg().toBuilder() - .setLastActivityTime(123L) - .build(); - TransportProtos.ToCoreMsg updatedToCoreMsg = actualToCoreMsg.toBuilder() - .setDeviceActivityMsg(updatedActivityMsg) - .build(); - - TransportProtos.DeviceActivityProto expectedDeviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) - .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) - .setLastActivityTime(123L) - .build(); - TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceActivityMsg(expectedDeviceActivityMsg) - .build(); - - assertThat(updatedToCoreMsg).isEqualTo(expectedToCoreMsg); - - then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); - then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); - then(ctxMock).shouldHaveNoMoreInteractions(); - } - - @Test - public void givenInactivityEventInConfig_whenOnMsg_thenOnDeviceInactivityCalledAndTellsSuccess() - throws TbNodeException { - // GIVEN - config.setEvent(TbMsgType.INACTIVITY_EVENT); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctxMock, nodeConfig); - given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); - given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); - given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); - - // WHEN - node.onMsg(ctxMock, DUMMY_MSG); - - // THEN - var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); - then(tbClusterServiceMock).should(times(1)) - .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); - - TransportProtos.DeviceInactivityProto expectedDeviceInactivityMsg = - TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) - .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) - .build(); - TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceInactivityMsg(expectedDeviceInactivityMsg) - .build(); - assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); - - then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); - then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); - then(ctxMock).shouldHaveNoMoreInteractions(); - } - - @Test - public void givenDisconnectEventInConfig_whenOnMsg_thenOnDeviceDisconnectCalledAndTellsSuccess() - throws TbNodeException { - // GIVEN - config.setEvent(TbMsgType.DISCONNECT_EVENT); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctxMock, nodeConfig); - given(ctxMock.getTenantId()).willReturn(DUMMY_TENANT_ID); - given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); - given(ctxMock.isLocalEntity(DUMMY_MSG.getOriginator())).willReturn(true); - - // WHEN - node.onMsg(ctxMock, DUMMY_MSG); - - // THEN - var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); - then(tbClusterServiceMock).should(times(1)) - .pushMsgToCore(eq(DUMMY_TENANT_ID), eq(DUMMY_MSG_ORIGINATOR), protoCaptor.capture(), any()); - - TransportProtos.DeviceDisconnectProto expectedDeviceDisconnectMsg = - TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(DUMMY_TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(DUMMY_TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DUMMY_MSG_ORIGINATOR.getId().getMostSignificantBits()) - .setDeviceIdLSB(DUMMY_MSG_ORIGINATOR.getId().getLeastSignificantBits()) - .build(); - TransportProtos.ToCoreMsg expectedToCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceDisconnectMsg(expectedDeviceDisconnectMsg) - .build(); - assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); - - then(ctxMock).should(times(1)).tellSuccess(DUMMY_MSG); - then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); - then(ctxMock).shouldHaveNoMoreInteractions(); + private static Stream provideSupportedEventsAndExpectedMessages() { + return Stream.of( + Arguments.of(TbMsgType.CONNECT_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceConnectMsg( + TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastConnectTime(METADATA_TS) + .build()).build()), + Arguments.of(TbMsgType.ACTIVITY_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceActivityMsg( + TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastActivityTime(METADATA_TS) + .build()).build()), + Arguments.of(TbMsgType.DISCONNECT_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceDisconnectMsg( + TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastDisconnectTime(METADATA_TS) + .build()).build()), + Arguments.of(TbMsgType.INACTIVITY_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceInactivityMsg( + TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastInactivityTime(METADATA_TS) + .build()).build()) + ); } } From 1303975c05f7bde0b64d85140f2e0dc61ef602c5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 19 Sep 2023 18:17:46 +0300 Subject: [PATCH 06/39] Revert removal of runtime exception --- .../server/service/queue/DefaultTbCoreConsumerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 7fb60f5da0..b4344c1cd6 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -624,7 +624,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService Date: Wed, 20 Sep 2023 14:38:37 +0300 Subject: [PATCH 07/39] Add mock test for new and a part of exisiting logic in core consumer; make changes according to review comments --- .../queue/DefaultTbCoreConsumerService.java | 8 +- .../state/DefaultDeviceStateService.java | 2 +- .../DefaultTbCoreConsumerServiceTest.java | 281 ++++++++++++++++++ .../state/DefaultDeviceStateServiceTest.java | 18 +- .../rule/engine/action/TbDeviceStateNode.java | 2 +- .../engine/action/TbDeviceStateNodeTest.java | 5 + 6 files changed, 297 insertions(+), 19 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index b4344c1cd6..8dbe9254eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -601,7 +601,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService SUPPORTED_EVENTS = Set.of( + static final Set SUPPORTED_EVENTS = Set.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 176136244d..cb81b4d2e4 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -97,6 +98,10 @@ public class TbDeviceStateNodeTest { // THEN assertThat(config.getEvent()).isEqualTo(TbMsgType.ACTIVITY_EVENT); + assertThat(TbDeviceStateNode.SUPPORTED_EVENTS).isEqualTo(Set.of( + TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, + TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT + )); } @Test From 0cfed35d5c0917d782fa49177c4cee93d651d7e4 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 20 Sep 2023 15:05:55 +0300 Subject: [PATCH 08/39] Fix licenses --- .../service/state/DefaultDeviceStateService.java | 8 ++++---- .../service/state/DefaultDeviceStateServiceTest.java | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) 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 10aea4d326..a141db1807 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 @@ -1,12 +1,12 @@ /** * Copyright © 2016-2023 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 - *

+ * + * 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. diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 600ab62ace..3913316e6e 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2023 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 - *

+ * + * 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. @@ -194,6 +194,8 @@ public class DefaultDeviceStateServiceTest { .metaData(new TbMsgMetaData()) .build(); + given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); + // WHEN service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); From 45f3448305baa8d862907e43e522fb9d36a76fcf Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 20 Sep 2023 15:15:11 +0300 Subject: [PATCH 09/39] Fix formatting --- .../state/DefaultDeviceStateService.java | 3 +- .../state/DefaultDeviceStateServiceTest.java | 58 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) 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 a141db1807..7e07a6534a 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 @@ -156,8 +156,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService provideParametersForUpdateInactivityStateIfExpired() { return Stream.of( - Arguments.of(false, 100, 70, 90, 70, 60, false, 90, false), + Arguments.of(false, 100, 70, 90, 70, 60, false, 90, false), - Arguments.of(false, 100, 40, 50, 70, 10, false, 50, false), + Arguments.of(false, 100, 40, 50, 70, 10, false, 50, false), - Arguments.of(false, 100, 25, 60, 75, 25, false, 60, false), + Arguments.of(false, 100, 25, 60, 75, 25, false, 60, false), - Arguments.of(false, 100, 60, 70, 10, 50, false, 70, false), + Arguments.of(false, 100, 60, 70, 10, 50, false, 70, false), - Arguments.of(false, 100, 10, 15, 90, 10, false, 15, false), + Arguments.of(false, 100, 10, 15, 90, 10, false, 15, false), - Arguments.of(false, 100, 0, 40, 75, 0, false, 40, false), + Arguments.of(false, 100, 0, 40, 75, 0, false, 40, false), - Arguments.of(true, 100, 90, 80, 80, 50, true, 80, false), + Arguments.of(true, 100, 90, 80, 80, 50, true, 80, false), - Arguments.of(true, 100, 95, 90, 10, 50, true, 90, false), + Arguments.of(true, 100, 95, 90, 10, 50, true, 90, false), - Arguments.of(true, 100, 10, 10, 90, 10, false, 100, true), + Arguments.of(true, 100, 10, 10, 90, 10, false, 100, true), - Arguments.of(true, 100, 10, 10, 90, 11, true, 10, false), + Arguments.of(true, 100, 10, 10, 90, 11, true, 10, false), - Arguments.of(true, 100, 15, 10, 85, 5, false, 100, true), + Arguments.of(true, 100, 15, 10, 85, 5, false, 100, true), - Arguments.of(true, 100, 15, 10, 75, 5, false, 100, true), + Arguments.of(true, 100, 15, 10, 75, 5, false, 100, true), - Arguments.of(true, 100, 95, 90, 5, 50, false, 100, true), + Arguments.of(true, 100, 95, 90, 5, 50, false, 100, true), - Arguments.of(true, 100, 0, 0, 101, 0, true, 0, false), + Arguments.of(true, 100, 0, 0, 101, 0, true, 0, false), - Arguments.of(true, 100, 0, 0, 100, 0, false, 100, true), + Arguments.of(true, 100, 0, 0, 100, 0, false, 100, true), - Arguments.of(true, 100, 0, 0, 99, 0, false, 100, true), + Arguments.of(true, 100, 0, 0, 99, 0, false, 100, true), - Arguments.of(true, 100, 0, 0, 120, 10, true, 0, false), + Arguments.of(true, 100, 0, 0, 120, 10, true, 0, false), - Arguments.of(true, 100, 50, 0, 100, 0, true, 0, false), + Arguments.of(true, 100, 50, 0, 100, 0, true, 0, false), - Arguments.of(true, 100, 10, 0, 91, 0, true, 0, false), + Arguments.of(true, 100, 10, 0, 91, 0, true, 0, false), - Arguments.of(true, 100, 90, 0, 10, 0, false, 100, true), + Arguments.of(true, 100, 90, 0, 10, 0, false, 100, true), - Arguments.of(true, 100, 100, 100, 1, 0, true, 100, false), + Arguments.of(true, 100, 100, 100, 1, 0, true, 100, false), - Arguments.of(true, 100, 100, 100, 100, 100, true, 100, false), + Arguments.of(true, 100, 100, 100, 100, 100, true, 100, false), - Arguments.of(false, 100, 59, 60, 30, 10, false, 60, false), + Arguments.of(false, 100, 59, 60, 30, 10, false, 60, false), - Arguments.of(true, 100, 60, 60, 30, 10, false, 100, true), + Arguments.of(true, 100, 60, 60, 30, 10, false, 100, true), - Arguments.of(true, 100, 61, 60, 30, 10, false, 100, true), + Arguments.of(true, 100, 61, 60, 30, 10, false, 100, true), - Arguments.of(true, 0, 0, 0, 1, 0, true, 0, false), + Arguments.of(true, 0, 0, 0, 1, 0, true, 0, false), - Arguments.of(true, 0, 0, 0, 0, 0, false, 0, true), + Arguments.of(true, 0, 0, 0, 0, 0, false, 0, true), - Arguments.of(true, 100, 90, 80, 20, 70, true, 80, false), + Arguments.of(true, 100, 90, 80, 20, 70, true, 80, false), - Arguments.of(true, 100, 80, 90, 30, 70, true, 90, false) + Arguments.of(true, 100, 80, 90, 30, 70, true, 90, false) ); } From d0cd4252d13b59cd20f792ccc2d1c2f693288a30 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 3 Oct 2023 18:13:50 +0300 Subject: [PATCH 10/39] Add EnumSet usage --- .../org/thingsboard/rule/engine/action/TbDeviceStateNode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 65787b3f61..7a4def297f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -30,6 +30,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import java.util.EnumSet; import java.util.Set; @Slf4j @@ -60,7 +61,7 @@ import java.util.Set; ) public class TbDeviceStateNode implements TbNode { - static final Set SUPPORTED_EVENTS = Set.of( + static final Set SUPPORTED_EVENTS = EnumSet.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); From 1e001dda0a3155e544d01fa4790e2278537aefee Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Oct 2023 15:15:39 +0300 Subject: [PATCH 11/39] Change log in case non-local originator --- .../org/thingsboard/rule/engine/action/TbDeviceStateNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 7a4def297f..c3593d6cc7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -83,7 +83,7 @@ public class TbDeviceStateNode implements TbNode { public void onMsg(TbContext ctx, TbMsg msg) { var originator = msg.getOriginator(); if (!ctx.isLocalEntity(originator)) { - log.warn("[{}] Node [{}] received message from non-local entity [{}]!", + log.warn("[{}][{}] Received message from non-local entity [{}]!", ctx.getTenantId().getId(), ctx.getSelfId().getId(), originator.getId()); ctx.ack(msg); return; From c9d97553b8761c8a4a26822958c21c81364888b1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 11:10:17 +0200 Subject: [PATCH 12/39] Update license headers with 2024 year. --- .../server/service/queue/DefaultTbCoreConsumerServiceTest.java | 2 +- .../thingsboard/server/queue/common/SimpleTbQueueCallback.java | 2 +- .../org/thingsboard/rule/engine/action/TbDeviceStateNode.java | 2 +- .../rule/engine/action/TbDeviceStateNodeConfiguration.java | 2 +- .../thingsboard/rule/engine/action/TbDeviceStateNodeTest.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java index 3ac86dac0c..fd7e2077d4 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java index 805511a603..5f86060d48 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index c3593d6cc7..63ebf09b9c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java index c89fcb076f..c23338fcd1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index cb81b4d2e4..246e5b3459 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. From e275678c22e4ba89eaca353117c140150fbdb016 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 12:55:09 +0200 Subject: [PATCH 13/39] Add check for negative and outdated inactivity event timestamp. --- .../state/DefaultDeviceStateService.java | 14 +++++- .../state/DefaultDeviceStateServiceTest.java | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) 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 458412b437..ee7b4a7e72 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 @@ -297,8 +297,20 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService givenOutdatedLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent() { + return Stream.of( + Arguments.of(0, 0), + Arguments.of(0, 100), + Arguments.of(50, 100), + Arguments.of(99, 100), + Arguments.of(100, 100) + ); + } + @Test public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { // GIVEN From ab21a1395b9554178dda383107231dbf75055ae3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 13:06:40 +0200 Subject: [PATCH 14/39] Remove unnecessary times(1) in test verifications --- .../state/DefaultDeviceStateServiceTest.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 816043c4d4..c924e6d93c 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -76,7 +76,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @@ -135,7 +134,7 @@ public class DefaultDeviceStateServiceTest { service.onDeviceInactivity(tenantId, deviceId, System.currentTimeMillis()); // THEN - then(service).should(times(1)).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); then(service).should(never()).fetchDeviceStateDataUsingSeparateRequests(deviceId); then(clusterService).shouldHaveNoInteractions(); then(notificationRuleProcessor).shouldHaveNoInteractions(); @@ -209,24 +208,24 @@ public class DefaultDeviceStateServiceTest { service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime); // THEN - then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + then(telemetrySubscriptionService).should().saveAttrAndNotify( eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(INACTIVITY_ALARM_TIME), eq(lastInactivityTime), any() ); - then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + then(telemetrySubscriptionService).should().saveAttrAndNotify( eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any() ); var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(clusterService).should(times(1)) + then(clusterService).should() .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); var actualMsg = msgCaptor.getValue(); assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); - then(notificationRuleProcessor).should(times(1)).process(notificationCaptor.capture()); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); var actualNotification = notificationCaptor.getValue(); assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); @@ -249,24 +248,24 @@ public class DefaultDeviceStateServiceTest { service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); // THEN - then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + then(telemetrySubscriptionService).should().saveAttrAndNotify( eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(INACTIVITY_ALARM_TIME), anyLong(), any() ); - then(telemetrySubscriptionService).should(times(1)).saveAttrAndNotify( + then(telemetrySubscriptionService).should().saveAttrAndNotify( eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any() ); var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(clusterService).should(times(1)) + then(clusterService).should() .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); var actualMsg = msgCaptor.getValue(); assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); - then(notificationRuleProcessor).should(times(1)).process(notificationCaptor.capture()); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); var actualNotification = notificationCaptor.getValue(); assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); @@ -287,7 +286,7 @@ public class DefaultDeviceStateServiceTest { willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); assertThat(deviceStateData).isEqualTo(deviceStateDataMock); - verify(service, times(1)).fetchDeviceStateDataUsingSeparateRequests(deviceId); + verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); } @Test @@ -513,7 +512,7 @@ public class DefaultDeviceStateServiceTest { } private void activityVerify(boolean isActive) { - verify(telemetrySubscriptionService, times(1)).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any()); + verify(telemetrySubscriptionService).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any()); } @Test @@ -869,7 +868,7 @@ public class DefaultDeviceStateServiceTest { } } - then(service).should(times(1)).fetchDeviceStateDataUsingSeparateRequests(deviceId); + then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId); } } From 1822703730ee2bace9aed2ebc907df8e79dea4c2 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 13:15:33 +0200 Subject: [PATCH 15/39] Add default overloads with current time millis for onDeviceConnect() and onDeviceDisconnect(). --- .../server/actors/device/DeviceActorMessageProcessor.java | 4 ++-- .../server/service/state/DeviceStateService.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) 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 c5625ee8ad..58bd82abb3 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 @@ -490,11 +490,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private void reportSessionOpen() { - systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId, System.currentTimeMillis()); + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId); } private void reportSessionClose() { - systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis()); + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); } private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java index 85d5645e1e..3ab5d29194 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java @@ -29,10 +29,18 @@ public interface DeviceStateService extends ApplicationListener Date: Wed, 24 Jan 2024 13:29:08 +0200 Subject: [PATCH 16/39] Refactor device state node test. --- .../engine/action/TbDeviceStateNodeTest.java | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 246e5b3459..2882493f84 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -88,15 +88,11 @@ public class TbDeviceStateNodeTest { @BeforeEach public void setUp() { node = new TbDeviceStateNode(); - config = new TbDeviceStateNodeConfiguration(); + config = new TbDeviceStateNodeConfiguration().defaultConfiguration(); } @Test public void givenDefaultConfiguration_whenInvoked_thenCorrectValuesAreSet() { - // GIVEN-WHEN - config = config.defaultConfiguration(); - - // THEN assertThat(config.getEvent()).isEqualTo(TbMsgType.ACTIVITY_EVENT); assertThat(TbDeviceStateNode.SUPPORTED_EVENTS).isEqualTo(Set.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, @@ -135,15 +131,15 @@ public class TbDeviceStateNodeTest { public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { // GIVEN var asset = new AssetId(UUID.randomUUID()); - var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, asset, new TbMsgMetaData(), "{}"); + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, asset, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); given(ctxMock.isLocalEntity(asset)).willReturn(true); // WHEN node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should(times(1)).isLocalEntity(asset); - then(ctxMock).should(times(1)).tellSuccess(msg); + then(ctxMock).should().isLocalEntity(asset); + then(ctxMock).should().tellSuccess(msg); then(ctxMock).shouldHaveNoMoreInteractions(); } @@ -158,18 +154,13 @@ public class TbDeviceStateNodeTest { node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should(times(1)).isLocalEntity(DEVICE_ID); - then(ctxMock).should().getTenantId(); - then(ctxMock).should().getSelfId(); - then(ctxMock).should(times(1)).ack(msg); + then(ctxMock).should().ack(msg); then(ctxMock).shouldHaveNoMoreInteractions(); } @ParameterizedTest @MethodSource("provideSupportedEventsAndExpectedMessages") - public void givenSupportedEvent_whenOnMsg_thenCorrectMsgIsSent( - TbMsgType event, TransportProtos.ToCoreMsg expectedToCoreMsg - ) { + public void givenSupportedEvent_whenOnMsg_thenCorrectMsgIsSent(TbMsgType event, TransportProtos.ToCoreMsg expectedToCoreMsg) { // GIVEN config.setEvent(event); var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); @@ -188,17 +179,16 @@ public class TbDeviceStateNodeTest { // THEN var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); var callbackCaptor = ArgumentCaptor.forClass(TbQueueCallback.class); - then(tbClusterServiceMock).should(times(1)) - .pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), protoCaptor.capture(), callbackCaptor.capture()); + then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), protoCaptor.capture(), callbackCaptor.capture()); TbQueueCallback actualCallback = callbackCaptor.getValue(); actualCallback.onSuccess(null); - then(ctxMock).should(times(1)).tellSuccess(msg); + then(ctxMock).should().tellSuccess(msg); var throwable = new Throwable(); actualCallback.onFailure(throwable); - then(ctxMock).should(times(1)).tellFailure(msg, throwable); + then(ctxMock).should().tellFailure(msg, throwable); assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); From 653ea4a80097c4ea3b7bce05f96d09f873003091 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 15:07:42 +0200 Subject: [PATCH 17/39] Add check for negative and outdated disconnect event timestamp. --- .../state/DefaultDeviceStateService.java | 16 +++- .../state/DefaultDeviceStateServiceTest.java | 90 ++++++++++++++++++- 2 files changed, 101 insertions(+), 5 deletions(-) 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 ee7b4a7e72..941ffef8fa 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 @@ -271,8 +271,19 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService givenOutdatedLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent() { + private static Stream provideOutdatedTimestamps() { return Stream.of( Arguments.of(0, 0), Arguments.of(0, 100), From c81d9438540905133ab8568af4bd361411a15990 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 16:11:19 +0200 Subject: [PATCH 18/39] Add check for negative and outdated connect event timestamp. --- .../state/DefaultDeviceStateService.java | 13 ++- .../state/DefaultDeviceStateServiceTest.java | 88 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) 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 941ffef8fa..b400b4f65a 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 @@ -226,8 +226,19 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService Date: Wed, 24 Jan 2024 16:25:26 +0200 Subject: [PATCH 19/39] Remove excessive brackets in switch-case --- .../rule/engine/action/TbDeviceStateNode.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 63ebf09b9c..ccc252c912 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -93,25 +93,20 @@ public class TbDeviceStateNode implements TbNode { return; } switch (event) { - case CONNECT_EVENT: { + case CONNECT_EVENT: sendDeviceConnectMsg(ctx, msg); break; - } - case ACTIVITY_EVENT: { + case ACTIVITY_EVENT: sendDeviceActivityMsg(ctx, msg); break; - } - case DISCONNECT_EVENT: { + case DISCONNECT_EVENT: sendDeviceDisconnectMsg(ctx, msg); break; - } - case INACTIVITY_EVENT: { + case INACTIVITY_EVENT: sendDeviceInactivityMsg(ctx, msg); break; - } - default: { + default: ctx.tellFailure(msg, new IllegalStateException("Configured event [" + event + "] is not supported!")); - } } } From 5e846116a885c655bb471776153f843c7f419a34 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 17:27:10 +0200 Subject: [PATCH 20/39] Use consumer instead of runnable in SimpleTbQueueCallback --- .../actors/ruleChain/DefaultTbContext.java | 40 ++++++++++++------- .../queue/common/SimpleTbQueueCallback.java | 6 +-- .../rule/engine/action/TbDeviceStateNode.java | 2 +- 3 files changed, 30 insertions(+), 18 deletions(-) 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 baae03761b..1e7d5d24e9 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 @@ -214,13 +214,19 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain"); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(onSuccess, t -> { - if (onFailure != null) { - onFailure.accept(t); - } else { - log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); - } - })); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback( + metadata -> { + if (onSuccess != null) { + onSuccess.run(); + } + }, + t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override @@ -306,13 +312,19 @@ class DefaultTbContext implements TbContext { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage)); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(onSuccess, t -> { - if (onFailure != null) { - onFailure.accept(t); - } else { - log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); - } - })); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback( + metadata -> { + if (onSuccess != null) { + onSuccess.run(); + } + }, + t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java index 5f86060d48..d153998ab4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java @@ -22,10 +22,10 @@ import java.util.function.Consumer; public class SimpleTbQueueCallback implements TbQueueCallback { - private final Runnable onSuccess; + private final Consumer onSuccess; private final Consumer onFailure; - public SimpleTbQueueCallback(Runnable onSuccess, Consumer onFailure) { + public SimpleTbQueueCallback(Consumer onSuccess, Consumer onFailure) { this.onSuccess = onSuccess; this.onFailure = onFailure; } @@ -33,7 +33,7 @@ public class SimpleTbQueueCallback implements TbQueueCallback { @Override public void onSuccess(TbQueueMsgMetadata metadata) { if (onSuccess != null) { - onSuccess.run(); + onSuccess.accept(metadata); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index ccc252c912..65d7e0979e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -183,7 +183,7 @@ public class TbDeviceStateNode implements TbNode { } private TbQueueCallback getMsgEnqueuedCallback(TbContext ctx, TbMsg msg) { - return new SimpleTbQueueCallback(() -> ctx.tellSuccess(msg), t -> ctx.tellFailure(msg, t)); + return new SimpleTbQueueCallback(metadata -> ctx.tellSuccess(msg), t -> ctx.tellFailure(msg, t)); } } From c65a9c2e0d559bc114d4b366b86af79a64d1c85d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 24 Jan 2024 17:58:24 +0200 Subject: [PATCH 21/39] Remove unnecessary originator locality check --- .../rule/engine/action/TbDeviceStateNode.java | 9 +-------- .../engine/action/TbDeviceStateNodeTest.java | 18 ------------------ 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 65d7e0979e..f28a3e4467 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -81,14 +81,7 @@ public class TbDeviceStateNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - var originator = msg.getOriginator(); - if (!ctx.isLocalEntity(originator)) { - log.warn("[{}][{}] Received message from non-local entity [{}]!", - ctx.getTenantId().getId(), ctx.getSelfId().getId(), originator.getId()); - ctx.ack(msg); - return; - } - if (!EntityType.DEVICE.equals(originator.getEntityType())) { + if (!EntityType.DEVICE.equals(msg.getOriginator().getEntityType())) { ctx.tellSuccess(msg); return; } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 2882493f84..988d8911a4 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -132,32 +132,15 @@ public class TbDeviceStateNodeTest { // GIVEN var asset = new AssetId(UUID.randomUUID()); var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, asset, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); - given(ctxMock.isLocalEntity(asset)).willReturn(true); // WHEN node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should().isLocalEntity(asset); then(ctxMock).should().tellSuccess(msg); then(ctxMock).shouldHaveNoMoreInteractions(); } - @Test - public void givenNonLocalOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { - // GIVEN - given(ctxMock.isLocalEntity(msg.getOriginator())).willReturn(false); - given(ctxMock.getSelfId()).willReturn(new RuleNodeId(UUID.randomUUID())); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - - // WHEN - node.onMsg(ctxMock, msg); - - // THEN - then(ctxMock).should().ack(msg); - then(ctxMock).shouldHaveNoMoreInteractions(); - } - @ParameterizedTest @MethodSource("provideSupportedEventsAndExpectedMessages") public void givenSupportedEvent_whenOnMsg_thenCorrectMsgIsSent(TbMsgType event, TransportProtos.ToCoreMsg expectedToCoreMsg) { @@ -171,7 +154,6 @@ public class TbDeviceStateNodeTest { } given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); - given(ctxMock.isLocalEntity(msg.getOriginator())).willReturn(true); // WHEN node.onMsg(ctxMock, msg); From d7a24648ff7ad463461552e317f67305c1460224 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 25 Jan 2024 12:11:53 +0200 Subject: [PATCH 22/39] Add stats for connect, disconnect and inactivity events --- .../queue/DefaultTbCoreConsumerService.java | 9 +++++++ .../service/queue/TbCoreConsumerStats.java | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 3a0e0e76d8..41f7cc012c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -660,6 +660,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService Date: Fri, 26 Jan 2024 13:37:08 +0200 Subject: [PATCH 23/39] Refactor core consumer tests: add test for device state message forwarding, split all-in-one tests into more but simpler test cases --- .../queue/DefaultTbCoreConsumerService.java | 2 +- .../DefaultTbCoreConsumerServiceTest.java | 352 +++++++++++++++--- 2 files changed, 291 insertions(+), 63 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 41f7cc012c..5c93351d0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -652,7 +652,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService Date: Fri, 26 Jan 2024 16:15:11 +0200 Subject: [PATCH 24/39] Add optimization using separate service which routes activity actions to device state service directly, when running in monolith. Refactor rule node tests --- .../server/actors/ActorSystemContext.java | 5 + .../actors/ruleChain/DefaultTbContext.java | 6 + ...ClusteredRuleEngineDeviceStateManager.java | 110 +++++++++++++ .../LocalRuleEngineDeviceStateManager.java | 85 ++++++++++ ...teredRuleEngineDeviceStateManagerTest.java | 150 ++++++++++++++++++ ...LocalRuleEngineDeviceStateManagerTest.java | 131 +++++++++++++++ .../thingsboard/server/common/msg/TbMsg.java | 2 +- .../api/RuleEngineDeviceStateManager.java | 32 ++++ .../rule/engine/api/TbContext.java | 2 + .../rule/engine/action/TbDeviceStateNode.java | 106 +++---------- .../engine/action/TbDeviceStateNodeTest.java | 108 ++++++------- 11 files changed, 592 insertions(+), 145 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java create mode 100644 application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java 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 31d9e477ac..6082c3f9f0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -31,6 +31,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -203,6 +204,10 @@ public class ActorSystemContext { @Getter private DeviceCredentialsService deviceCredentialsService; + @Autowired + @Getter + private RuleEngineDeviceStateManager deviceStateManager; + @Autowired @Getter private TbTenantProfileCache tenantProfileCache; 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 1e7d5d24e9..b97201005c 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 @@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -683,6 +684,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceCredentialsService(); } + @Override + public RuleEngineDeviceStateManager getDeviceStateManager() { + return mainCtx.getDeviceStateManager(); + } + @Override public TbClusterService getClusterService() { return mainCtx.getClusterService(); diff --git a/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..2a73d3f544 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2024 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.state; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import org.thingsboard.server.queue.util.TbRuleEngineComponent; + +@Slf4j +@Service +@TbRuleEngineComponent +@RequiredArgsConstructor +public class ClusteredRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { + + private final TbClusterService clusterService; + + @Override + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { + var tenantUuid = tenantId.getId(); + var deviceUuid = deviceId.getId(); + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastConnectTime(connectTime) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + log.trace("[{}][{}] Sending device connect message to core. Connect time: [{}].", tenantUuid, deviceUuid, connectTime); + clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + + @Override + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { + var tenantUuid = tenantId.getId(); + var deviceUuid = deviceId.getId(); + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastActivityTime(activityTime) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + log.trace("[{}][{}] Sending device activity message to core. Activity time: [{}].", tenantUuid, deviceUuid, activityTime); + clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + + @Override + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { + var tenantUuid = tenantId.getId(); + var deviceUuid = deviceId.getId(); + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastDisconnectTime(disconnectTime) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + log.trace("[{}][{}] Sending device disconnect message to core. Disconnect time: [{}].", tenantUuid, deviceUuid, disconnectTime); + clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + + @Override + public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { + var tenantUuid = tenantId.getId(); + var deviceUuid = deviceId.getId(); + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantUuid.getMostSignificantBits()) + .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) + .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) + .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) + .setLastInactivityTime(inactivityTime) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + log.trace("[{}][{}] Sending device inactivity message to core. Inactivity time: [{}].", tenantUuid, deviceUuid, inactivityTime); + clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..180aea9ecb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2024 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.state; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Slf4j +@Service +@Primary +@TbCoreComponent +@RequiredArgsConstructor +public class LocalRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { + + private final DeviceStateService deviceStateService; + + @Override + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { + try { + deviceStateService.onDeviceConnect(tenantId, deviceId, connectTime); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device connect event. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } + + @Override + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { + try { + deviceStateService.onDeviceActivity(tenantId, deviceId, activityTime); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device activity event. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } + + @Override + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { + try { + deviceStateService.onDeviceDisconnect(tenantId, deviceId, disconnectTime); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device disconnect event. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } + + @Override + public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { + try { + deviceStateService.onDeviceInactivity(tenantId, deviceId, inactivityTime); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device inactivity event. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java new file mode 100644 index 0000000000..e972b49347 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2024 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.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; + +@ExtendWith(MockitoExtension.class) +public class ClusteredRuleEngineDeviceStateManagerTest { + + @Mock + private static TbClusterService tbClusterServiceMock; + @Mock + private static TbCallback tbCallbackMock; + @Mock + private static TbQueueMsgMetadata metadataMock; + @Captor + private static ArgumentCaptor queueCallbackCaptor; + private static ClusteredRuleEngineDeviceStateManager deviceStateManager; + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); + private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); + private static final long EVENT_TS = System.currentTimeMillis(); + + @BeforeEach + public void setup() { + deviceStateManager = new ClusteredRuleEngineDeviceStateManager(tbClusterServiceMock); + } + + @ParameterizedTest + @MethodSource + public void givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { + // WHEN + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + TbQueueCallback callback = queueCallbackCaptor.getValue(); + callback.onSuccess(metadataMock); + then(tbCallbackMock).should().onSuccess(); + + var runtimeException = new RuntimeException("Something bad happened!"); + callback.onFailure(runtimeException); + then(tbCallbackMock).should().onFailure(runtimeException); + } + + private static Stream givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastConnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastActivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastDisconnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastInactivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java new file mode 100644 index 0000000000..db4d71f995 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2024 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.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class LocalRuleEngineDeviceStateManagerTest { + + @Mock + private static DeviceStateService deviceStateServiceMock; + @Mock + private static TbCallback tbCallbackMock; + private static LocalRuleEngineDeviceStateManager deviceStateManager; + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); + private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); + private static final long EVENT_TS = System.currentTimeMillis(); + private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException("Something bad happened!"); + + @BeforeEach + public void setup() { + deviceStateManager = new LocalRuleEngineDeviceStateManager(deviceStateServiceMock); + } + + @ParameterizedTest + @MethodSource + public void givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { + // WHEN + onDeviceAction.run(); + + // THEN + actionVerification.run(); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + private static Stream givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + + @ParameterizedTest + @MethodSource + public void givenProcessingFailure_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnFailureCallback( + Runnable exceptionThrowSetup, Runnable onDeviceAction, Runnable actionVerification + ) { + // GIVEN + exceptionThrowSetup.run(); + + // WHEN + onDeviceAction.run(); + + // THEN + actionVerification.run(); + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); + } + + private static Stream givenProcessingFailure_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnFailureCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 1e06d75b15..d81393cf25 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -335,7 +335,7 @@ public final class TbMsg implements Serializable { this.originator = originator; if (customerId == null || customerId.isNullUid()) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { - this.customerId = (CustomerId) originator; + this.customerId = new CustomerId(originator.getId()); } else { this.customerId = null; } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..82c2916408 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 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.rule.engine.api; + +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; + +public interface RuleEngineDeviceStateManager { + + void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback); + + void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback); + + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback); + + void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 59f5b128a7..32fb81ccd9 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -280,6 +280,8 @@ public interface TbContext { DeviceCredentialsService getDeviceCredentialsService(); + RuleEngineDeviceStateManager getDeviceStateManager(); + TbClusterService getClusterService(); DashboardService getDashboardService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index f28a3e4467..7168ad3c0f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.action; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -23,12 +24,12 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import org.thingsboard.server.common.msg.queue.TbCallback; import java.util.EnumSet; import java.util.Set; @@ -61,7 +62,7 @@ import java.util.Set; ) public class TbDeviceStateNode implements TbNode { - static final Set SUPPORTED_EVENTS = EnumSet.of( + private static final Set SUPPORTED_EVENTS = EnumSet.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); @@ -85,98 +86,41 @@ public class TbDeviceStateNode implements TbNode { ctx.tellSuccess(msg); return; } + + TenantId tenantId = ctx.getTenantId(); + DeviceId deviceId = (DeviceId) msg.getOriginator(); + RuleEngineDeviceStateManager deviceStateManager = ctx.getDeviceStateManager(); + switch (event) { case CONNECT_EVENT: - sendDeviceConnectMsg(ctx, msg); + deviceStateManager.onDeviceConnect(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); break; case ACTIVITY_EVENT: - sendDeviceActivityMsg(ctx, msg); + deviceStateManager.onDeviceActivity(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); break; case DISCONNECT_EVENT: - sendDeviceDisconnectMsg(ctx, msg); + deviceStateManager.onDeviceDisconnect(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); break; case INACTIVITY_EVENT: - sendDeviceInactivityMsg(ctx, msg); + deviceStateManager.onDeviceInactivity(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); break; default: ctx.tellFailure(msg, new IllegalStateException("Configured event [" + event + "] is not supported!")); } } - private void sendDeviceConnectMsg(TbContext ctx, TbMsg msg) { - var tenantUuid = ctx.getTenantId().getId(); - var deviceUuid = msg.getOriginator().getId(); - var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastConnectTime(msg.getMetaDataTs()) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceConnectMsg(deviceConnectMsg) - .build(); - ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) - ); - } + private TbCallback getMsgEnqueuedCallback(TbContext ctx, TbMsg msg) { + return new TbCallback() { + @Override + public void onSuccess() { + ctx.tellSuccess(msg); + } - private void sendDeviceActivityMsg(TbContext ctx, TbMsg msg) { - var tenantUuid = ctx.getTenantId().getId(); - var deviceUuid = msg.getOriginator().getId(); - var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastActivityTime(msg.getMetaDataTs()) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceActivityMsg(deviceActivityMsg) - .build(); - ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) - ); - } - - private void sendDeviceDisconnectMsg(TbContext ctx, TbMsg msg) { - var tenantUuid = ctx.getTenantId().getId(); - var deviceUuid = msg.getOriginator().getId(); - var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastDisconnectTime(msg.getMetaDataTs()) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceDisconnectMsg(deviceDisconnectMsg) - .build(); - ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) - ); - } - - private void sendDeviceInactivityMsg(TbContext ctx, TbMsg msg) { - var tenantUuid = ctx.getTenantId().getId(); - var deviceUuid = msg.getOriginator().getId(); - var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastInactivityTime(msg.getMetaDataTs()) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceInactivityMsg(deviceInactivityMsg) - .build(); - ctx.getClusterService().pushMsgToCore( - ctx.getTenantId(), msg.getOriginator(), toCoreMsg, getMsgEnqueuedCallback(ctx, msg) - ); - } - - private TbQueueCallback getMsgEnqueuedCallback(TbContext ctx, TbMsg msg) { - return new SimpleTbQueueCallback(metadata -> ctx.tellSuccess(msg), t -> ctx.tellFailure(msg, t)); + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + }; } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 988d8911a4..e967ec49b5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -20,28 +20,28 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.common.msg.queue.TbCallback; -import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -51,7 +51,6 @@ import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) public class TbDeviceStateNodeTest { @@ -59,7 +58,9 @@ public class TbDeviceStateNodeTest { @Mock private TbContext ctxMock; @Mock - private TbClusterService tbClusterServiceMock; + private static RuleEngineDeviceStateManager deviceStateManagerMock; + @Captor + private static ArgumentCaptor callbackCaptor; private TbDeviceStateNode node; private TbDeviceStateNodeConfiguration config; @@ -94,10 +95,6 @@ public class TbDeviceStateNodeTest { @Test public void givenDefaultConfiguration_whenInvoked_thenCorrectValuesAreSet() { assertThat(config.getEvent()).isEqualTo(TbMsgType.ACTIVITY_EVENT); - assertThat(TbDeviceStateNode.SUPPORTED_EVENTS).isEqualTo(Set.of( - TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, - TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT - )); } @Test @@ -113,10 +110,14 @@ public class TbDeviceStateNodeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()); } - @Test - public void givenUnsupportedEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException() { + @ParameterizedTest + @EnumSource( + value = TbMsgType.class, + names = {"CONNECT_EVENT", "ACTIVITY_EVENT", "DISCONNECT_EVENT", "INACTIVITY_EVENT"}, + mode = EnumSource.Mode.EXCLUDE + ) + public void givenUnsupportedEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException(TbMsgType unsupportedEvent) { // GIVEN - var unsupportedEvent = TbMsgType.TO_SERVER_RPC_REQUEST; config.setEvent(unsupportedEvent); var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); @@ -127,11 +128,23 @@ public class TbDeviceStateNodeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()); } - @Test - public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered() { + @ParameterizedTest + @EnumSource(value = EntityType.class, names = "DEVICE", mode = EnumSource.Mode.EXCLUDE) + public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered(EntityType unsupportedType) { // GIVEN - var asset = new AssetId(UUID.randomUUID()); - var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, asset, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + var nonDeviceOriginator = new EntityId() { + + @Override + public UUID getId() { + return UUID.randomUUID(); + } + + @Override + public EntityType getEntityType() { + return unsupportedType; + } + }; + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, nonDeviceOriginator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); // WHEN node.onMsg(ctxMock, msg); @@ -142,10 +155,10 @@ public class TbDeviceStateNodeTest { } @ParameterizedTest - @MethodSource("provideSupportedEventsAndExpectedMessages") - public void givenSupportedEvent_whenOnMsg_thenCorrectMsgIsSent(TbMsgType event, TransportProtos.ToCoreMsg expectedToCoreMsg) { + @MethodSource + public void givenInactivityEventAndDeviceOriginator_whenOnMsg_thenOnDeviceInactivityIsCalledWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { // GIVEN - config.setEvent(event); + config.setEvent(supportedEventType); var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); try { node.init(ctxMock, nodeConfig); @@ -153,65 +166,34 @@ public class TbDeviceStateNodeTest { fail("Node failed to initialize!", e); } given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(ctxMock.getClusterService()).willReturn(tbClusterServiceMock); + given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); // WHEN node.onMsg(ctxMock, msg); // THEN - var protoCaptor = ArgumentCaptor.forClass(TransportProtos.ToCoreMsg.class); - var callbackCaptor = ArgumentCaptor.forClass(TbQueueCallback.class); - then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), protoCaptor.capture(), callbackCaptor.capture()); + actionVerification.run(); - TbQueueCallback actualCallback = callbackCaptor.getValue(); + TbCallback actualCallback = callbackCaptor.getValue(); - actualCallback.onSuccess(null); + actualCallback.onSuccess(); then(ctxMock).should().tellSuccess(msg); var throwable = new Throwable(); actualCallback.onFailure(throwable); then(ctxMock).should().tellFailure(msg, throwable); - assertThat(expectedToCoreMsg).isEqualTo(protoCaptor.getValue()); - then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + then(deviceStateManagerMock).shouldHaveNoMoreInteractions(); then(ctxMock).shouldHaveNoMoreInteractions(); } - private static Stream provideSupportedEventsAndExpectedMessages() { + private static Stream givenInactivityEventAndDeviceOriginator_whenOnMsg_thenOnDeviceInactivityIsCalledWithCorrectCallback() { return Stream.of( - Arguments.of(TbMsgType.CONNECT_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceConnectMsg( - TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastConnectTime(METADATA_TS) - .build()).build()), - Arguments.of(TbMsgType.ACTIVITY_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceActivityMsg( - TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastActivityTime(METADATA_TS) - .build()).build()), - Arguments.of(TbMsgType.DISCONNECT_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceDisconnectMsg( - TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastDisconnectTime(METADATA_TS) - .build()).build()), - Arguments.of(TbMsgType.INACTIVITY_EVENT, TransportProtos.ToCoreMsg.newBuilder().setDeviceInactivityMsg( - TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastInactivityTime(METADATA_TS) - .build()).build()) + Arguments.of(TbMsgType.CONNECT_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceConnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.ACTIVITY_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.DISCONNECT_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceDisconnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.INACTIVITY_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceInactivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())) ); } From b1b0e44c7fda9118cea95b6715430cf1641943bc Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 26 Jan 2024 17:04:49 +0200 Subject: [PATCH 25/39] Add executor to process activity events in a dedicated thread not to avoid blocking main consumer --- .../queue/DefaultTbCoreConsumerService.java | 64 +++++++++++-------- .../DefaultTbCoreConsumerServiceTest.java | 23 +++++++ 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 5c93351d0a..9942476c01 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.service.queue; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -148,6 +151,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceConnect(tenantId, deviceId, deviceConnectMsg.getLastConnectTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device connect message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); } void forwardToStateService(TransportProtos.DeviceActivityProto deviceActivityMsg, TbCallback callback) { @@ -680,13 +688,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device activity message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(new RuntimeException("Failed to update device activity for device [" + deviceId.getId() + "]!", t)); + }); } void forwardToStateService(TransportProtos.DeviceDisconnectProto deviceDisconnectMsg, TbCallback callback) { @@ -695,13 +703,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceDisconnect(tenantId, deviceId, deviceDisconnectMsg.getLastDisconnectTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device disconnect message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); } void forwardToStateService(TransportProtos.DeviceInactivityProto deviceInactivityMsg, TbCallback callback) { @@ -710,13 +718,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceInactivity(tenantId, deviceId, deviceInactivityMsg.getLastInactivityTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device inactivity message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); } private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) { diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java index 19166f75d2..621d4c5a91 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.service.queue; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +32,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.state.DeviceStateService; import java.util.UUID; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -52,12 +56,31 @@ public class DefaultTbCoreConsumerServiceTest { private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); private final long time = System.currentTimeMillis(); + private ListeningExecutorService executor; + @Mock private DefaultTbCoreConsumerService defaultTbCoreConsumerServiceMock; @BeforeEach public void setup() { + executor = MoreExecutors.newDirectExecutorService(); ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stateService", stateServiceMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "deviceActivityEventsExecutor", executor); + } + + @AfterEach + public void cleanup() { + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(10L, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } } @Test From b19d7f1406775e0afd511e46ecd08a4594907064 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 26 Jan 2024 17:08:50 +0200 Subject: [PATCH 26/39] Clarify information about event ts in node description --- .../rule/engine/action/TbDeviceStateNode.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 7168ad3c0f..d8cc81679c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -39,12 +39,9 @@ import java.util.Set; type = ComponentType.ACTION, name = "device state", nodeDescription = "Triggers device connectivity events", - nodeDetails = "If incoming message originator is a device," + - " registers configured event for that device in the Device State Service," + - " which sends appropriate message to the Rule Engine." + - " If metadata ts property is present, it will be used as event timestamp." + - " Incoming message is forwarded using the Success chain," + - " unless an unexpected error occurs during message processing" + + nodeDetails = "If incoming message originator is a device, registers configured event for that device in the Device State Service, which sends appropriate message to the Rule Engine." + + " If metadata ts property is present, it will be used as event timestamp. Otherwise, the message timestamp will be used." + + " Incoming message is forwarded using the Success chain, unless an unexpected error occurs during message processing" + " then incoming message is forwarded using the Failure chain." + "
" + "Supported device connectivity events are:" + @@ -54,8 +51,7 @@ import java.util.Set; "

  • Activity event
  • " + "
  • Inactivity event
  • " + "" + - "This node is particularly useful when device isn't using transports to receive data," + - " such as when fetching data from external API or computing new data within the rule chain.", + "This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.", configClazz = TbDeviceStateNodeConfiguration.class, uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeDeviceStateConfig" From d1a9c4432acb5398497a01b148e01018933fa0b3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 30 Jan 2024 11:56:10 +0200 Subject: [PATCH 27/39] Add rate limit of at most one event per second per originator in node --- .../server/common/data/msg/TbMsgType.java | 1 + .../rule/engine/action/TbDeviceStateNode.java | 86 +++++++- .../engine/action/TbDeviceStateNodeTest.java | 187 +++++++++++++++--- 3 files changed, 239 insertions(+), 35 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index 763f245a09..c7eeef158d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -76,6 +76,7 @@ public enum TbMsgType { DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), DELAY_TIMEOUT_SELF_MSG(null, true), MSG_COUNT_SELF_MSG(null, true), + DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG(null, true), // Custom or N/A type: NA; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index d8cc81679c..a74df13f20 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.action; +import com.google.common.base.Stopwatch; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleNode; @@ -29,10 +30,15 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import java.time.Duration; import java.util.EnumSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; @Slf4j @RuleNode( @@ -61,7 +67,11 @@ public class TbDeviceStateNode implements TbNode { private static final Set SUPPORTED_EVENTS = EnumSet.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); + private static final Duration ONE_SECOND = Duration.ofSeconds(1L); + private static final Duration ENTRY_EXPIRATION_TIME = Duration.ofDays(1L); + private Stopwatch stopwatch; + private ConcurrentMap lastActivityEventTimestamps; private TbMsgType event; @Override @@ -74,31 +84,82 @@ public class TbDeviceStateNode implements TbNode { throw new TbNodeException("Unsupported event: " + event, true); } this.event = event; + lastActivityEventTimestamps = new ConcurrentHashMap<>(); + stopwatch = Stopwatch.createStarted(); + scheduleCleanupMsg(ctx); } @Override public void onMsg(TbContext ctx, TbMsg msg) { + if (msg.isTypeOf(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG)) { + removeStaleEntries(); + scheduleCleanupMsg(ctx); + ctx.ack(msg); + return; + } + if (!EntityType.DEVICE.equals(msg.getOriginator().getEntityType())) { ctx.tellSuccess(msg); return; } + DeviceId originator = new DeviceId(msg.getOriginator().getId()); + + lastActivityEventTimestamps.compute(originator, (__, lastEventTs) -> { + Duration now = stopwatch.elapsed(); + + if (lastEventTs == null) { + sendEvent(ctx, originator, msg); + return now; + } + + Duration elapsedSinceLastEventSent = now.minus(lastEventTs); + if (elapsedSinceLastEventSent.compareTo(ONE_SECOND) < 0) { + ctx.tellSuccess(msg); + return lastEventTs; + } + + sendEvent(ctx, originator, msg); + + return now; + }); + } + + private void scheduleCleanupMsg(TbContext ctx) { + TbMsg cleanupMsg = ctx.newMsg( + null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, ctx.getSelfId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING + ); + ctx.tellSelf(cleanupMsg, Duration.ofHours(1L).toMillis()); + } + + private void removeStaleEntries() { + lastActivityEventTimestamps.entrySet().removeIf(entry -> { + Duration now = stopwatch.elapsed(); + Duration lastEventTs = entry.getValue(); + Duration elapsedSinceLastEventSent = now.minus(lastEventTs); + return elapsedSinceLastEventSent.compareTo(ENTRY_EXPIRATION_TIME) > 0; + }); + } + + private void sendEvent(TbContext ctx, DeviceId originator, TbMsg msg) { TenantId tenantId = ctx.getTenantId(); - DeviceId deviceId = (DeviceId) msg.getOriginator(); + long eventTs = msg.getMetaDataTs(); + RuleEngineDeviceStateManager deviceStateManager = ctx.getDeviceStateManager(); + TbCallback callback = getMsgEnqueuedCallback(ctx, msg); switch (event) { case CONNECT_EVENT: - deviceStateManager.onDeviceConnect(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); + deviceStateManager.onDeviceConnect(tenantId, originator, eventTs, callback); break; case ACTIVITY_EVENT: - deviceStateManager.onDeviceActivity(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); + deviceStateManager.onDeviceActivity(tenantId, originator, eventTs, callback); break; case DISCONNECT_EVENT: - deviceStateManager.onDeviceDisconnect(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); + deviceStateManager.onDeviceDisconnect(tenantId, originator, eventTs, callback); break; case INACTIVITY_EVENT: - deviceStateManager.onDeviceInactivity(tenantId, deviceId, msg.getMetaDataTs(), getMsgEnqueuedCallback(ctx, msg)); + deviceStateManager.onDeviceInactivity(tenantId, originator, eventTs, callback); break; default: ctx.tellFailure(msg, new IllegalStateException("Configured event [" + event + "] is not supported!")); @@ -119,4 +180,19 @@ public class TbDeviceStateNode implements TbNode { }; } + @Override + public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { + lastActivityEventTimestamps.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); + } + + @Override + public void destroy() { + if (lastActivityEventTimestamps != null) { + lastActivityEventTimestamps.clear(); + lastActivityEventTimestamps = null; + } + stopwatch = null; + event = null; + } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index e967ec49b5..af72927e3c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.action; +import com.google.common.base.Stopwatch; +import com.google.common.base.Ticker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,29 +28,35 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import java.time.Duration; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; @@ -62,28 +70,33 @@ public class TbDeviceStateNodeTest { @Captor private static ArgumentCaptor callbackCaptor; private TbDeviceStateNode node; + private RuleNodeId nodeId; private TbDeviceStateNodeConfiguration config; private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); private static final long METADATA_TS = System.currentTimeMillis(); + private TbMsg cleanupMsg; private TbMsg msg; + private long nowNanos; + private final Ticker controlledTicker = new Ticker() { + @Override + public long read() { + return nowNanos; + } + }; @BeforeEach public void setup() { - var device = new Device(); - device.setTenantId(TENANT_ID); - device.setId(DEVICE_ID); - device.setName("My humidity sensor"); - device.setType("Humidity sensor"); - device.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); var metaData = new TbMsgMetaData(); - metaData.putValue("deviceName", device.getName()); - metaData.putValue("deviceType", device.getType()); + metaData.putValue("deviceName", "My humidity sensor"); + metaData.putValue("deviceType", "Humidity sensor"); metaData.putValue("ts", String.valueOf(METADATA_TS)); var data = JacksonUtil.newObjectNode(); data.put("humidity", 58.3); - msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), metaData, JacksonUtil.toString(data)); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, JacksonUtil.toString(data)); + nodeId = new RuleNodeId(UUID.randomUUID()); + cleanupMsg = TbMsg.newMsg(null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, nodeId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); } @BeforeEach @@ -99,17 +112,121 @@ public class TbDeviceStateNodeTest { @Test public void givenNullEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException() { - // GIVEN - config.setEvent(null); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - // WHEN-THEN - assertThatThrownBy(() -> node.init(ctxMock, nodeConfig)) + // GIVEN-WHEN-THEN + assertThatThrownBy(() -> initNode(null)) .isInstanceOf(TbNodeException.class) .hasMessage("Event cannot be null!") .matches(e -> ((TbNodeException) e).isUnrecoverable()); } + @Test + public void givenValidConfig_whenInit_thenSchedulesCleanupMsg() { + // GIVEN + given(ctxMock.getSelfId()).willReturn(nodeId); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + + // WHEN + try { + initNode(TbMsgType.ACTIVITY_EVENT); + } catch (Exception e) { + fail("Node failed to initialize."); + } + + // THEN + verifyCleanupMsgSent(); + } + + @Test + public void givenCleanupMsg_whenOnMsg_thenCleansStaleEntries() { + // GIVEN + given(ctxMock.getSelfId()).willReturn(nodeId); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + + ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); + ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); + + Stopwatch stopwatch = Stopwatch.createStarted(controlledTicker); + ReflectionTestUtils.setField(node, "stopwatch", stopwatch); + + // WHEN + Duration expirationTime = Duration.ofDays(1L); + + DeviceId staleId = DEVICE_ID; + Duration staleTs = Duration.ofHours(4L); + lastActivityEventTimestamps.put(staleId, staleTs); + + DeviceId goodId = new DeviceId(UUID.randomUUID()); + Duration goodTs = staleTs.plus(expirationTime); + lastActivityEventTimestamps.put(goodId, goodTs); + + nowNanos = staleTs.toNanos() + expirationTime.toNanos() + 1; + node.onMsg(ctxMock, cleanupMsg); + + // THEN + assertThat(lastActivityEventTimestamps) + .containsKey(goodId) + .doesNotContainKey(staleId) + .size().isOne(); + + verifyCleanupMsgSent(); + then(ctxMock).should().ack(cleanupMsg); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenMsgArrivedTooFast_whenOnMsg_thenRateLimitsThisMsg() { + // GIVEN + ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); + ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); + + Stopwatch stopwatch = Stopwatch.createStarted(controlledTicker); + ReflectionTestUtils.setField(node, "stopwatch", stopwatch); + + // WHEN + Duration firstEventTs = Duration.ofMillis(1000L); + lastActivityEventTimestamps.put(DEVICE_ID, firstEventTs); + + Duration tooFastEventTs = firstEventTs.plus(Duration.ofMillis(999L)); + nowNanos = tooFastEventTs.toNanos(); + node.onMsg(ctxMock, msg); + + // THEN + Duration actualEventTs = lastActivityEventTimestamps.get(DEVICE_ID); + assertThat(actualEventTs).isEqualTo(firstEventTs); + + then(ctxMock).should().tellSuccess(msg); + then(ctxMock).shouldHaveNoMoreInteractions(); + then(deviceStateManagerMock).shouldHaveNoInteractions(); + } + + @Test + public void givenHasNonLocalDevices_whenOnPartitionChange_thenRemovesEntriesForNonLocalDevices() { + // GIVEN + ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); + ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); + + lastActivityEventTimestamps.put(DEVICE_ID, Duration.ofHours(24L)); + given(ctxMock.isLocalEntity(eq(DEVICE_ID))).willReturn(true); + + DeviceId nonLocalDeviceId1 = new DeviceId(UUID.randomUUID()); + lastActivityEventTimestamps.put(nonLocalDeviceId1, Duration.ofHours(30L)); + given(ctxMock.isLocalEntity(eq(nonLocalDeviceId1))).willReturn(false); + + DeviceId nonLocalDeviceId2 = new DeviceId(UUID.randomUUID()); + lastActivityEventTimestamps.put(nonLocalDeviceId2, Duration.ofHours(32L)); + given(ctxMock.isLocalEntity(eq(nonLocalDeviceId2))).willReturn(false); + + // WHEN + node.onPartitionChangeMsg(ctxMock, new PartitionChangeMsg(ServiceType.TB_RULE_ENGINE)); + + // THEN + assertThat(lastActivityEventTimestamps) + .containsKey(DEVICE_ID) + .doesNotContainKey(nonLocalDeviceId1) + .doesNotContainKey(nonLocalDeviceId2) + .size().isOne(); + } + @ParameterizedTest @EnumSource( value = TbMsgType.class, @@ -117,12 +234,8 @@ public class TbDeviceStateNodeTest { mode = EnumSource.Mode.EXCLUDE ) public void givenUnsupportedEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException(TbMsgType unsupportedEvent) { - // GIVEN - config.setEvent(unsupportedEvent); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - // WHEN-THEN - assertThatThrownBy(() -> node.init(ctxMock, nodeConfig)) + // GIVEN-WHEN-THEN + assertThatThrownBy(() -> initNode(unsupportedEvent)) .isInstanceOf(TbNodeException.class) .hasMessage("Unsupported event: " + unsupportedEvent) .matches(e -> ((TbNodeException) e).isUnrecoverable()); @@ -156,17 +269,19 @@ public class TbDeviceStateNodeTest { @ParameterizedTest @MethodSource - public void givenInactivityEventAndDeviceOriginator_whenOnMsg_thenOnDeviceInactivityIsCalledWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { + public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { // GIVEN - config.setEvent(supportedEventType); - var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + given(ctxMock.getSelfId()).willReturn(nodeId); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); + try { - node.init(ctxMock, nodeConfig); + initNode(supportedEventType); } catch (TbNodeException e) { fail("Node failed to initialize!", e); } - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); + verifyCleanupMsgSent(); // WHEN node.onMsg(ctxMock, msg); @@ -188,7 +303,7 @@ public class TbDeviceStateNodeTest { then(ctxMock).shouldHaveNoMoreInteractions(); } - private static Stream givenInactivityEventAndDeviceOriginator_whenOnMsg_thenOnDeviceInactivityIsCalledWithCorrectCallback() { + private static Stream givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback() { return Stream.of( Arguments.of(TbMsgType.CONNECT_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceConnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), Arguments.of(TbMsgType.ACTIVITY_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), @@ -197,4 +312,16 @@ public class TbDeviceStateNodeTest { ); } + private void initNode(TbMsgType event) throws TbNodeException { + config.setEvent(event); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + } + + private void verifyCleanupMsgSent() { + then(ctxMock).should().getSelfId(); + then(ctxMock).should().newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING)); + then(ctxMock).should().tellSelf(eq(cleanupMsg), eq(Duration.ofHours(1L).toMillis())); + } + } From 2159430c1b50383a4463a8377bee3f6f8b6e9cb3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 30 Jan 2024 12:22:37 +0200 Subject: [PATCH 28/39] Fix test for message types with null rule node connections --- .../org/thingsboard/server/common/data/msg/TbMsgTypeTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 9dc7110340..ebf717fb63 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -22,6 +22,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.NA; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; @@ -53,6 +54,7 @@ class TbMsgTypeTest { DEDUPLICATION_TIMEOUT_SELF_MSG, DELAY_TIMEOUT_SELF_MSG, MSG_COUNT_SELF_MSG, + DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, NA ); From 691a6b5d6a1cc0acd300eca5d9396e296207bf82 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 2 Feb 2024 10:52:38 +0200 Subject: [PATCH 29/39] Polishing after review --- .../queue/DefaultTbCoreConsumerService.java | 2 +- .../server/common/data/msg/TbMsgType.java | 2 +- .../server/common/data/msg/TbMsgTypeTest.java | 18 +++++++++--------- .../rule/engine/action/TbDeviceStateNode.java | 15 +++++++-------- .../engine/action/TbDeviceStateNodeTest.java | 11 +++++------ 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 9942476c01..6db0e16f28 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -199,7 +199,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService typesWithNullRuleNodeConnection = List.of( ALARM, ALARM_DELETE, @@ -54,7 +54,7 @@ class TbMsgTypeTest { DEDUPLICATION_TIMEOUT_SELF_MSG, DELAY_TIMEOUT_SELF_MSG, MSG_COUNT_SELF_MSG, - DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, + DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, NA ); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index a74df13f20..9d6901ea01 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -69,6 +69,7 @@ public class TbDeviceStateNode implements TbNode { ); private static final Duration ONE_SECOND = Duration.ofSeconds(1L); private static final Duration ENTRY_EXPIRATION_TIME = Duration.ofDays(1L); + private static final Duration ENTRY_CLEANUP_PERIOD = Duration.ofHours(1L); private Stopwatch stopwatch; private ConcurrentMap lastActivityEventTimestamps; @@ -91,10 +92,9 @@ public class TbDeviceStateNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.isTypeOf(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG)) { + if (msg.isTypeOf(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG)) { removeStaleEntries(); scheduleCleanupMsg(ctx); - ctx.ack(msg); return; } @@ -109,7 +109,7 @@ public class TbDeviceStateNode implements TbNode { Duration now = stopwatch.elapsed(); if (lastEventTs == null) { - sendEvent(ctx, originator, msg); + sendEventAndTell(ctx, originator, msg); return now; } @@ -119,17 +119,16 @@ public class TbDeviceStateNode implements TbNode { return lastEventTs; } - sendEvent(ctx, originator, msg); - + sendEventAndTell(ctx, originator, msg); return now; }); } private void scheduleCleanupMsg(TbContext ctx) { TbMsg cleanupMsg = ctx.newMsg( - null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, ctx.getSelfId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING + null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, ctx.getSelfId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING ); - ctx.tellSelf(cleanupMsg, Duration.ofHours(1L).toMillis()); + ctx.tellSelf(cleanupMsg, ENTRY_CLEANUP_PERIOD.toMillis()); } private void removeStaleEntries() { @@ -141,7 +140,7 @@ public class TbDeviceStateNode implements TbNode { }); } - private void sendEvent(TbContext ctx, DeviceId originator, TbMsg msg) { + private void sendEventAndTell(TbContext ctx, DeviceId originator, TbMsg msg) { TenantId tenantId = ctx.getTenantId(); long eventTs = msg.getMetaDataTs(); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index af72927e3c..1ce49b3a7b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -96,7 +96,7 @@ public class TbDeviceStateNodeTest { data.put("humidity", 58.3); msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, JacksonUtil.toString(data)); nodeId = new RuleNodeId(UUID.randomUUID()); - cleanupMsg = TbMsg.newMsg(null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG, nodeId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + cleanupMsg = TbMsg.newMsg(null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, nodeId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); } @BeforeEach @@ -123,7 +123,7 @@ public class TbDeviceStateNodeTest { public void givenValidConfig_whenInit_thenSchedulesCleanupMsg() { // GIVEN given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); // WHEN try { @@ -140,7 +140,7 @@ public class TbDeviceStateNodeTest { public void givenCleanupMsg_whenOnMsg_thenCleansStaleEntries() { // GIVEN given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); @@ -169,7 +169,6 @@ public class TbDeviceStateNodeTest { .size().isOne(); verifyCleanupMsgSent(); - then(ctxMock).should().ack(cleanupMsg); then(ctxMock).shouldHaveNoMoreInteractions(); } @@ -272,7 +271,7 @@ public class TbDeviceStateNodeTest { public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { // GIVEN given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); @@ -320,7 +319,7 @@ public class TbDeviceStateNodeTest { private void verifyCleanupMsgSent() { then(ctxMock).should().getSelfId(); - then(ctxMock).should().newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING)); + then(ctxMock).should().newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING)); then(ctxMock).should().tellSelf(eq(cleanupMsg), eq(Duration.ofHours(1L).toMillis())); } From 46dca971e119c031f63cd5fab40390d6cf9576a0 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 2 Feb 2024 11:19:20 +0200 Subject: [PATCH 30/39] Add check to ensure that received inactivity time is not outdated relative to current activity time --- .../state/DefaultDeviceStateService.java | 8 ++++- .../state/DefaultDeviceStateServiceTest.java | 32 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) 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 b400b4f65a..30c59b3da8 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 @@ -327,10 +327,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService Date: Fri, 2 Feb 2024 16:03:35 +0200 Subject: [PATCH 31/39] Add test to ensure message timestamp is used as event timestamp when metadata ts property is not present --- .../thingsboard/server/common/msg/TbMsg.java | 5 ++++ .../engine/action/TbDeviceStateNodeTest.java | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index d81393cf25..cbf9ae34b3 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -131,6 +131,11 @@ public final class TbMsg implements Serializable { metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, long ts) { + return new TbMsg(null, UUID.randomUUID(), ts, type, originator, null, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + // REALLY NEW MSG /** diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 1ce49b3a7b..d66babea9a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -55,6 +55,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; @@ -75,7 +76,7 @@ public class TbDeviceStateNodeTest { private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); - private static final long METADATA_TS = System.currentTimeMillis(); + private static final long METADATA_TS = 123L; private TbMsg cleanupMsg; private TbMsg msg; private long nowNanos; @@ -266,6 +267,28 @@ public class TbDeviceStateNodeTest { then(ctxMock).shouldHaveNoMoreInteractions(); } + @Test + public void givenMetadataDoesNotContainTs_whenOnMsg_thenMsgTsIsUsedAsEventTs() { + // GIVEN + try { + initNode(TbMsgType.ACTIVITY_EVENT); + } catch (TbNodeException e) { + fail("Node failed to initialize!", e); + } + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); + + long msgTs = METADATA_TS + 1; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, msgTs); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(msgTs), any()); + } + @ParameterizedTest @MethodSource public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { From 094cff6174039febbf3fc3a4b64bc99085f79942 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 14 Feb 2024 14:01:20 +0200 Subject: [PATCH 32/39] Tell failure if originator is not a device --- .../thingsboard/rule/engine/action/TbDeviceStateNode.java | 7 +++++-- .../rule/engine/action/TbDeviceStateNodeTest.java | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 9d6901ea01..b0f426d905 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -98,8 +98,11 @@ public class TbDeviceStateNode implements TbNode { return; } - if (!EntityType.DEVICE.equals(msg.getOriginator().getEntityType())) { - ctx.tellSuccess(msg); + EntityType originatorEntityType = msg.getOriginator().getEntityType(); + if (!EntityType.DEVICE.equals(originatorEntityType)) { + ctx.tellFailure(msg, new IllegalArgumentException( + "Unsupported originator entity type: [" + originatorEntityType + "]. Only DEVICE entity type is supported." + )); return; } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index d66babea9a..19cb3c55bd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -263,7 +263,12 @@ public class TbDeviceStateNodeTest { node.onMsg(ctxMock, msg); // THEN - then(ctxMock).should().tellSuccess(msg); + var exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + then(ctxMock).should().tellFailure(eq(msg), exceptionCaptor.capture()); + assertThat(exceptionCaptor.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported originator entity type: [" + unsupportedType + "]. Only DEVICE entity type is supported."); + then(ctxMock).shouldHaveNoMoreInteractions(); } From c5c18202c21dfb7714b67efb8ce501e50773bdaf Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 14:13:40 +0200 Subject: [PATCH 33/39] Improve connectivity events routing logic --- ...ClusteredRuleEngineDeviceStateManager.java | 110 -------- .../DefaultRuleEngineDeviceStateManager.java | 194 +++++++++++++ .../LocalRuleEngineDeviceStateManager.java | 85 ------ ...teredRuleEngineDeviceStateManagerTest.java | 150 ---------- ...faultRuleEngineDeviceStateManagerTest.java | 266 ++++++++++++++++++ ...LocalRuleEngineDeviceStateManagerTest.java | 131 --------- 6 files changed, 460 insertions(+), 476 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java deleted file mode 100644 index 2a73d3f544..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManager.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright © 2016-2024 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.state; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.common.SimpleTbQueueCallback; -import org.thingsboard.server.queue.util.TbRuleEngineComponent; - -@Slf4j -@Service -@TbRuleEngineComponent -@RequiredArgsConstructor -public class ClusteredRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { - - private final TbClusterService clusterService; - - @Override - public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { - var tenantUuid = tenantId.getId(); - var deviceUuid = deviceId.getId(); - var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastConnectTime(connectTime) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceConnectMsg(deviceConnectMsg) - .build(); - log.trace("[{}][{}] Sending device connect message to core. Connect time: [{}].", tenantUuid, deviceUuid, connectTime); - clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); - } - - @Override - public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { - var tenantUuid = tenantId.getId(); - var deviceUuid = deviceId.getId(); - var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastActivityTime(activityTime) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceActivityMsg(deviceActivityMsg) - .build(); - log.trace("[{}][{}] Sending device activity message to core. Activity time: [{}].", tenantUuid, deviceUuid, activityTime); - clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); - } - - @Override - public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { - var tenantUuid = tenantId.getId(); - var deviceUuid = deviceId.getId(); - var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastDisconnectTime(disconnectTime) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceDisconnectMsg(deviceDisconnectMsg) - .build(); - log.trace("[{}][{}] Sending device disconnect message to core. Disconnect time: [{}].", tenantUuid, deviceUuid, disconnectTime); - clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); - } - - @Override - public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { - var tenantUuid = tenantId.getId(); - var deviceUuid = deviceId.getId(); - var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(tenantUuid.getMostSignificantBits()) - .setTenantIdLSB(tenantUuid.getLeastSignificantBits()) - .setDeviceIdMSB(deviceUuid.getMostSignificantBits()) - .setDeviceIdLSB(deviceUuid.getLeastSignificantBits()) - .setLastInactivityTime(inactivityTime) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceInactivityMsg(deviceInactivityMsg) - .build(); - log.trace("[{}][{}] Sending device inactivity message to core. Inactivity time: [{}].", tenantUuid, deviceUuid, inactivityTime); - clusterService.pushMsgToCore(tenantId, deviceId, toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..087f470fed --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java @@ -0,0 +1,194 @@ +/** + * Copyright © 2016-2024 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.state; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { + + private final TbServiceInfoProvider serviceInfoProvider; + private final PartitionService partitionService; + + private final Optional deviceStateService; + private final TbClusterService clusterService; + + public DefaultRuleEngineDeviceStateManager( + TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, + Optional deviceStateServiceOptional, TbClusterService clusterService + ) { + this.serviceInfoProvider = serviceInfoProvider; + this.partitionService = partitionService; + this.deviceStateService = deviceStateServiceOptional; + this.clusterService = clusterService; + } + + @Getter + private abstract static class ConnectivityEventInfo { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final long eventTime; + + private ConnectivityEventInfo(TenantId tenantId, DeviceId deviceId, long eventTime) { + this.tenantId = tenantId; + this.deviceId = deviceId; + this.eventTime = eventTime; + } + + abstract void forwardToLocalService(); + + abstract TransportProtos.ToCoreMsg toQueueMsg(); + + } + + @Override + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, connectTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceConnect(tenantId, deviceId, connectTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(connectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, activityTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceActivity(tenantId, deviceId, activityTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(activityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, disconnectTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceDisconnect(tenantId, deviceId, disconnectTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(disconnectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, inactivityTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceInactivity(tenantId, deviceId, inactivityTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(inactivityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + } + }, callback); + } + + private void routeEvent(ConnectivityEventInfo eventInfo, TbCallback callback) { + var tenantId = eventInfo.getTenantId(); + var deviceId = eventInfo.getDeviceId(); + long eventTime = eventInfo.getEventTime(); + + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); + if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) { + log.info("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + try { + eventInfo.forwardToLocalService(); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device connectivity event. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } else { + TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg(); + log.info("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java deleted file mode 100644 index 180aea9ecb..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManager.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright © 2016-2024 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.state; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Slf4j -@Service -@Primary -@TbCoreComponent -@RequiredArgsConstructor -public class LocalRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { - - private final DeviceStateService deviceStateService; - - @Override - public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { - try { - deviceStateService.onDeviceConnect(tenantId, deviceId, connectTime); - } catch (Exception e) { - log.error("[{}][{}] Failed to process device connect event. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime, e); - callback.onFailure(e); - return; - } - callback.onSuccess(); - } - - @Override - public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { - try { - deviceStateService.onDeviceActivity(tenantId, deviceId, activityTime); - } catch (Exception e) { - log.error("[{}][{}] Failed to process device activity event. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime, e); - callback.onFailure(e); - return; - } - callback.onSuccess(); - } - - @Override - public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { - try { - deviceStateService.onDeviceDisconnect(tenantId, deviceId, disconnectTime); - } catch (Exception e) { - log.error("[{}][{}] Failed to process device disconnect event. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime, e); - callback.onFailure(e); - return; - } - callback.onSuccess(); - } - - @Override - public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { - try { - deviceStateService.onDeviceInactivity(tenantId, deviceId, inactivityTime); - } catch (Exception e) { - log.error("[{}][{}] Failed to process device inactivity event. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime, e); - callback.onFailure(e); - return; - } - callback.onSuccess(); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java deleted file mode 100644 index e972b49347..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/state/ClusteredRuleEngineDeviceStateManagerTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Copyright © 2016-2024 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.state; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; - -import java.util.UUID; -import java.util.stream.Stream; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.then; - -@ExtendWith(MockitoExtension.class) -public class ClusteredRuleEngineDeviceStateManagerTest { - - @Mock - private static TbClusterService tbClusterServiceMock; - @Mock - private static TbCallback tbCallbackMock; - @Mock - private static TbQueueMsgMetadata metadataMock; - @Captor - private static ArgumentCaptor queueCallbackCaptor; - private static ClusteredRuleEngineDeviceStateManager deviceStateManager; - - private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); - private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); - private static final long EVENT_TS = System.currentTimeMillis(); - - @BeforeEach - public void setup() { - deviceStateManager = new ClusteredRuleEngineDeviceStateManager(tbClusterServiceMock); - } - - @ParameterizedTest - @MethodSource - public void givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { - // WHEN - onDeviceAction.run(); - - // THEN - actionVerification.run(); - - TbQueueCallback callback = queueCallbackCaptor.getValue(); - callback.onSuccess(metadataMock); - then(tbCallbackMock).should().onSuccess(); - - var runtimeException = new RuntimeException("Something bad happened!"); - callback.onFailure(runtimeException); - then(tbCallbackMock).should().onFailure(runtimeException); - } - - private static Stream givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback() { - return Stream.of( - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> { - var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastConnectTime(EVENT_TS) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceConnectMsg(deviceConnectMsg) - .build(); - then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); - } - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> { - var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastActivityTime(EVENT_TS) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceActivityMsg(deviceActivityMsg) - .build(); - then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); - } - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> { - var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastDisconnectTime(EVENT_TS) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceDisconnectMsg(deviceDisconnectMsg) - .build(); - then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); - } - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> { - var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) - .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) - .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) - .setLastInactivityTime(EVENT_TS) - .build(); - var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() - .setDeviceInactivityMsg(deviceInactivityMsg) - .build(); - then(tbClusterServiceMock).should().pushMsgToCore(eq(TENANT_ID), eq(DEVICE_ID), eq(toCoreMsg), queueCallbackCaptor.capture()); - } - ) - ); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java new file mode 100644 index 0000000000..c97305c170 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java @@ -0,0 +1,266 @@ +/** + * Copyright © 2016-2024 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.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class DefaultRuleEngineDeviceStateManagerTest { + + @Mock + private static DeviceStateService deviceStateServiceMock; + @Mock + private static TbCallback tbCallbackMock; + @Mock + private static TbClusterService clusterServiceMock; + @Mock + private static TbQueueMsgMetadata metadataMock; + + @Mock + private TbServiceInfoProvider serviceInfoProviderMock; + @Mock + private PartitionService partitionServiceMock; + + @Captor + private static ArgumentCaptor queueCallbackCaptor; + + private static DefaultRuleEngineDeviceStateManager deviceStateManager; + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); + private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); + private static final long EVENT_TS = System.currentTimeMillis(); + private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException("Something bad happened!"); + private static final TopicPartitionInfo MY_TPI = TopicPartitionInfo.builder().myPartition(true).build(); + private static final TopicPartitionInfo EXTERNAL_TPI = TopicPartitionInfo.builder().myPartition(false).build(); + + @BeforeEach + public void setup() { + deviceStateManager = new DefaultRuleEngineDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to local service and event processed has succeeded, " + + "when onDeviceX() is called, then should route event to local service and call onSuccess() callback.") + @MethodSource + public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { + // GIVEN + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI); + + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + then(clusterServiceMock).shouldHaveNoInteractions(); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + private static Stream givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to local service and event processed has failed, " + + "when onDeviceX() is called, then should route event to local service and call onFailure() callback.") + @MethodSource + public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback( + Runnable exceptionThrowSetup, Runnable onDeviceAction, Runnable actionVerification + ) { + // GIVEN + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI); + + exceptionThrowSetup.run(); + + // WHEN + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + then(clusterServiceMock).shouldHaveNoInteractions(); + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); + } + + private static Stream givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to external service, " + + "when onDeviceX() is called, then should send correct queue message to external service with correct callback object.") + @MethodSource + public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback(Runnable onDeviceAction, Runnable actionVerification) { + // WHEN + ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty()); + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(false); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(EXTERNAL_TPI); + + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + TbQueueCallback callback = queueCallbackCaptor.getValue(); + callback.onSuccess(metadataMock); + then(tbCallbackMock).should().onSuccess(); + callback.onFailure(RUNTIME_EXCEPTION); + then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); + } + + private static Stream givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastConnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastActivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastDisconnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastInactivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java deleted file mode 100644 index db4d71f995..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/state/LocalRuleEngineDeviceStateManagerTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright © 2016-2024 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.state; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; - -import java.util.UUID; -import java.util.stream.Stream; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; - -@ExtendWith(MockitoExtension.class) -public class LocalRuleEngineDeviceStateManagerTest { - - @Mock - private static DeviceStateService deviceStateServiceMock; - @Mock - private static TbCallback tbCallbackMock; - private static LocalRuleEngineDeviceStateManager deviceStateManager; - - private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); - private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); - private static final long EVENT_TS = System.currentTimeMillis(); - private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException("Something bad happened!"); - - @BeforeEach - public void setup() { - deviceStateManager = new LocalRuleEngineDeviceStateManager(deviceStateServiceMock); - } - - @ParameterizedTest - @MethodSource - public void givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { - // WHEN - onDeviceAction.run(); - - // THEN - actionVerification.run(); - then(tbCallbackMock).should().onSuccess(); - then(tbCallbackMock).should(never()).onFailure(any()); - } - - private static Stream givenProcessingSuccess_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnSuccessCallback() { - return Stream.of( - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) - ) - ); - } - - @ParameterizedTest - @MethodSource - public void givenProcessingFailure_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnFailureCallback( - Runnable exceptionThrowSetup, Runnable onDeviceAction, Runnable actionVerification - ) { - // GIVEN - exceptionThrowSetup.run(); - - // WHEN - onDeviceAction.run(); - - // THEN - actionVerification.run(); - then(tbCallbackMock).should(never()).onSuccess(); - then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); - } - - private static Stream givenProcessingFailure_whenOnDeviceAction_thenCallsDeviceStateServiceAndOnFailureCallback() { - return Stream.of( - Arguments.of( - (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), - (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), - (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), - (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) - ), - Arguments.of( - (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), - (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), - (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) - ) - ); - } - -} From e55d14325a7658193eca00115c7f918098a8406c Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 17:09:32 +0200 Subject: [PATCH 34/39] Refactor rate limiting to be configurable and use existing class --- .../server/actors/ActorSystemContext.java | 4 + .../actors/ruleChain/DefaultTbContext.java | 5 + .../src/main/resources/thingsboard.yml | 9 ++ .../rule/engine/api/TbContext.java | 2 + .../rule/engine/action/TbDeviceStateNode.java | 91 +++++--------- .../engine/action/TbDeviceStateNodeTest.java | 114 +++++------------- 6 files changed, 80 insertions(+), 145 deletions(-) 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 6082c3f9f0..2a4f132be8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -561,6 +561,10 @@ public class ActorSystemContext { @Getter private boolean externalNodeForceAck; + @Value("${state.rule.node.deviceState.rateLimit:1:1,30:60,60:3600}") + @Getter + private String deviceStateNodeRateLimitConfig; + @Getter @Setter private TbActorSystem actorSystem; 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 b97201005c..9110ab794d 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 @@ -689,6 +689,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceStateManager(); } + @Override + public String getDeviceStateNodeRateLimitConfig() { + return mainCtx.getDeviceStateNodeRateLimitConfig(); + } + @Override public TbClusterService getClusterService() { return mainCtx.getClusterService(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 82a347d41c..5b22a3b7be 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -794,6 +794,15 @@ state: # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data. # 0 means time-to-live mechanism is disabled. telemetryTtl: "${STATE_TELEMETRY_TTL:0}" + # Configuration properties for rule nodes related to device activity state + rule: + node: + # Device state rule node + deviceState: + # Defines the rate at which device connectivity events can be triggered. + # Comma-separated list of capacity:duration pairs that define bandwidth capacity and refill duration for token bucket rate limit algorithm. + # Refill is set to be greedy. Please refer to Bucket4j library documentation for more details. + rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}" # Tbel parameters tbel: diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 32fb81ccd9..fc2980650b 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -282,6 +282,8 @@ public interface TbContext { RuleEngineDeviceStateManager getDeviceStateManager(); + String getDeviceStateNodeRateLimitConfig(); + TbClusterService getClusterService(); DashboardService getDashboardService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index b0f426d905..7925178b33 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -15,8 +15,8 @@ */ package org.thingsboard.rule.engine.action; -import com.google.common.base.Stopwatch; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -28,17 +28,15 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.tools.TbRateLimits; -import java.time.Duration; import java.util.EnumSet; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Slf4j @RuleNode( @@ -47,8 +45,8 @@ import java.util.concurrent.ConcurrentMap; nodeDescription = "Triggers device connectivity events", nodeDetails = "If incoming message originator is a device, registers configured event for that device in the Device State Service, which sends appropriate message to the Rule Engine." + " If metadata ts property is present, it will be used as event timestamp. Otherwise, the message timestamp will be used." + - " Incoming message is forwarded using the Success chain, unless an unexpected error occurs during message processing" + - " then incoming message is forwarded using the Failure chain." + + " If originator entity type is not DEVICE or unexpected error happened during processing, then incoming message is forwarded using Failure chain." + + " If rate of connectivity events for a given originator is too high, then incoming message is forwarded using Rate limited chain. " + "
    " + "Supported device connectivity events are:" + "
      " + @@ -59,6 +57,7 @@ import java.util.concurrent.ConcurrentMap; "
    " + "This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.", configClazz = TbDeviceStateNodeConfiguration.class, + relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, "Rate limited"}, uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeDeviceStateConfig" ) @@ -67,12 +66,9 @@ public class TbDeviceStateNode implements TbNode { private static final Set SUPPORTED_EVENTS = EnumSet.of( TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT ); - private static final Duration ONE_SECOND = Duration.ofSeconds(1L); - private static final Duration ENTRY_EXPIRATION_TIME = Duration.ofDays(1L); - private static final Duration ENTRY_CLEANUP_PERIOD = Duration.ofHours(1L); - - private Stopwatch stopwatch; - private ConcurrentMap lastActivityEventTimestamps; + private static final String DEFAULT_RATE_LIMIT_CONFIG = "1:1,30:60,60:3600"; + private ConcurrentReferenceHashMap rateLimits; + private String rateLimitConfig; private TbMsgType event; @Override @@ -85,19 +81,19 @@ public class TbDeviceStateNode implements TbNode { throw new TbNodeException("Unsupported event: " + event, true); } this.event = event; - lastActivityEventTimestamps = new ConcurrentHashMap<>(); - stopwatch = Stopwatch.createStarted(); - scheduleCleanupMsg(ctx); + rateLimits = new ConcurrentReferenceHashMap<>(); + String deviceStateNodeRateLimitConfig = ctx.getDeviceStateNodeRateLimitConfig(); + try { + rateLimitConfig = new TbRateLimits(deviceStateNodeRateLimitConfig).getConfiguration(); + } catch (Exception e) { + log.error("[{}][{}] Invalid rate limit configuration provided: [{}]. Will use default value [{}].", + ctx.getTenantId().getId(), ctx.getSelfId().getId(), deviceStateNodeRateLimitConfig, DEFAULT_RATE_LIMIT_CONFIG, e); + rateLimitConfig = DEFAULT_RATE_LIMIT_CONFIG; + } } @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.isTypeOf(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG)) { - removeStaleEntries(); - scheduleCleanupMsg(ctx); - return; - } - EntityType originatorEntityType = msg.getOriginator().getEntityType(); if (!EntityType.DEVICE.equals(originatorEntityType)) { ctx.tellFailure(msg, new IllegalArgumentException( @@ -105,41 +101,18 @@ public class TbDeviceStateNode implements TbNode { )); return; } - DeviceId originator = new DeviceId(msg.getOriginator().getId()); - - lastActivityEventTimestamps.compute(originator, (__, lastEventTs) -> { - Duration now = stopwatch.elapsed(); - - if (lastEventTs == null) { + rateLimits.compute(originator, (__, rateLimit) -> { + if (rateLimit == null) { + rateLimit = new TbRateLimits(rateLimitConfig); + } + boolean isNotRateLimited = rateLimit.tryConsume(); + if (isNotRateLimited) { sendEventAndTell(ctx, originator, msg); - return now; + } else { + ctx.tellNext(msg, "Rate limited"); } - - Duration elapsedSinceLastEventSent = now.minus(lastEventTs); - if (elapsedSinceLastEventSent.compareTo(ONE_SECOND) < 0) { - ctx.tellSuccess(msg); - return lastEventTs; - } - - sendEventAndTell(ctx, originator, msg); - return now; - }); - } - - private void scheduleCleanupMsg(TbContext ctx) { - TbMsg cleanupMsg = ctx.newMsg( - null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, ctx.getSelfId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING - ); - ctx.tellSelf(cleanupMsg, ENTRY_CLEANUP_PERIOD.toMillis()); - } - - private void removeStaleEntries() { - lastActivityEventTimestamps.entrySet().removeIf(entry -> { - Duration now = stopwatch.elapsed(); - Duration lastEventTs = entry.getValue(); - Duration elapsedSinceLastEventSent = now.minus(lastEventTs); - return elapsedSinceLastEventSent.compareTo(ENTRY_EXPIRATION_TIME) > 0; + return rateLimit; }); } @@ -184,16 +157,16 @@ public class TbDeviceStateNode implements TbNode { @Override public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { - lastActivityEventTimestamps.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); + rateLimits.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); } @Override public void destroy() { - if (lastActivityEventTimestamps != null) { - lastActivityEventTimestamps.clear(); - lastActivityEventTimestamps = null; + if (rateLimits != null) { + rateLimits.clear(); + rateLimits = null; } - stopwatch = null; + rateLimitConfig = null; event = null; } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index 19cb3c55bd..9352d64809 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -15,8 +15,6 @@ */ package org.thingsboard.rule.engine.action; -import com.google.common.base.Stopwatch; -import com.google.common.base.Ticker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +27,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.TbContext; @@ -45,11 +44,9 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.tools.TbRateLimits; -import java.time.Duration; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -57,9 +54,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) public class TbDeviceStateNodeTest { @@ -71,21 +69,12 @@ public class TbDeviceStateNodeTest { @Captor private static ArgumentCaptor callbackCaptor; private TbDeviceStateNode node; - private RuleNodeId nodeId; private TbDeviceStateNodeConfiguration config; private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); private static final long METADATA_TS = 123L; - private TbMsg cleanupMsg; private TbMsg msg; - private long nowNanos; - private final Ticker controlledTicker = new Ticker() { - @Override - public long read() { - return nowNanos; - } - }; @BeforeEach public void setup() { @@ -96,8 +85,6 @@ public class TbDeviceStateNodeTest { var data = JacksonUtil.newObjectNode(); data.put("humidity", 58.3); msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, JacksonUtil.toString(data)); - nodeId = new RuleNodeId(UUID.randomUUID()); - cleanupMsg = TbMsg.newMsg(null, TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, nodeId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); } @BeforeEach @@ -121,80 +108,42 @@ public class TbDeviceStateNodeTest { } @Test - public void givenValidConfig_whenInit_thenSchedulesCleanupMsg() { + public void givenInvalidRateLimitConfig_whenInit_thenUsesDefaultConfig() { // GIVEN - given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("invalid rate limit config"); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelfId()).willReturn(new RuleNodeId(UUID.randomUUID())); // WHEN try { initNode(TbMsgType.ACTIVITY_EVENT); } catch (Exception e) { - fail("Node failed to initialize."); + fail("Node failed to initialize!", e); } // THEN - verifyCleanupMsgSent(); - } - - @Test - public void givenCleanupMsg_whenOnMsg_thenCleansStaleEntries() { - // GIVEN - given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); - - ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); - ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); - - Stopwatch stopwatch = Stopwatch.createStarted(controlledTicker); - ReflectionTestUtils.setField(node, "stopwatch", stopwatch); - - // WHEN - Duration expirationTime = Duration.ofDays(1L); - - DeviceId staleId = DEVICE_ID; - Duration staleTs = Duration.ofHours(4L); - lastActivityEventTimestamps.put(staleId, staleTs); - - DeviceId goodId = new DeviceId(UUID.randomUUID()); - Duration goodTs = staleTs.plus(expirationTime); - lastActivityEventTimestamps.put(goodId, goodTs); - - nowNanos = staleTs.toNanos() + expirationTime.toNanos() + 1; - node.onMsg(ctxMock, cleanupMsg); - - // THEN - assertThat(lastActivityEventTimestamps) - .containsKey(goodId) - .doesNotContainKey(staleId) - .size().isOne(); - - verifyCleanupMsgSent(); - then(ctxMock).shouldHaveNoMoreInteractions(); + String actualRateLimitConfig = (String) ReflectionTestUtils.getField(node, "rateLimitConfig"); + assertThat(actualRateLimitConfig).isEqualTo("1:1,30:60,60:3600"); } @Test public void givenMsgArrivedTooFast_whenOnMsg_thenRateLimitsThisMsg() { // GIVEN - ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); - ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); + ConcurrentReferenceHashMap rateLimits = new ConcurrentReferenceHashMap<>(); + ReflectionTestUtils.setField(node, "rateLimits", rateLimits); - Stopwatch stopwatch = Stopwatch.createStarted(controlledTicker); - ReflectionTestUtils.setField(node, "stopwatch", stopwatch); + var rateLimitMock = mock(TbRateLimits.class); + rateLimits.put(DEVICE_ID, rateLimitMock); + + given(rateLimitMock.tryConsume()).willReturn(false); // WHEN - Duration firstEventTs = Duration.ofMillis(1000L); - lastActivityEventTimestamps.put(DEVICE_ID, firstEventTs); - - Duration tooFastEventTs = firstEventTs.plus(Duration.ofMillis(999L)); - nowNanos = tooFastEventTs.toNanos(); node.onMsg(ctxMock, msg); // THEN - Duration actualEventTs = lastActivityEventTimestamps.get(DEVICE_ID); - assertThat(actualEventTs).isEqualTo(firstEventTs); - - then(ctxMock).should().tellSuccess(msg); + then(ctxMock).should().tellNext(msg, "Rate limited"); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); then(ctxMock).shouldHaveNoMoreInteractions(); then(deviceStateManagerMock).shouldHaveNoInteractions(); } @@ -202,25 +151,25 @@ public class TbDeviceStateNodeTest { @Test public void givenHasNonLocalDevices_whenOnPartitionChange_thenRemovesEntriesForNonLocalDevices() { // GIVEN - ConcurrentMap lastActivityEventTimestamps = new ConcurrentHashMap<>(); - ReflectionTestUtils.setField(node, "lastActivityEventTimestamps", lastActivityEventTimestamps); + ConcurrentReferenceHashMap rateLimits = new ConcurrentReferenceHashMap<>(); + ReflectionTestUtils.setField(node, "rateLimits", rateLimits); - lastActivityEventTimestamps.put(DEVICE_ID, Duration.ofHours(24L)); + rateLimits.put(DEVICE_ID, new TbRateLimits("1:1")); given(ctxMock.isLocalEntity(eq(DEVICE_ID))).willReturn(true); DeviceId nonLocalDeviceId1 = new DeviceId(UUID.randomUUID()); - lastActivityEventTimestamps.put(nonLocalDeviceId1, Duration.ofHours(30L)); + rateLimits.put(nonLocalDeviceId1, new TbRateLimits("2:2")); given(ctxMock.isLocalEntity(eq(nonLocalDeviceId1))).willReturn(false); DeviceId nonLocalDeviceId2 = new DeviceId(UUID.randomUUID()); - lastActivityEventTimestamps.put(nonLocalDeviceId2, Duration.ofHours(32L)); + rateLimits.put(nonLocalDeviceId2, new TbRateLimits("3:3")); given(ctxMock.isLocalEntity(eq(nonLocalDeviceId2))).willReturn(false); // WHEN node.onPartitionChangeMsg(ctxMock, new PartitionChangeMsg(ServiceType.TB_RULE_ENGINE)); // THEN - assertThat(lastActivityEventTimestamps) + assertThat(rateLimits) .containsKey(DEVICE_ID) .doesNotContainKey(nonLocalDeviceId1) .doesNotContainKey(nonLocalDeviceId2) @@ -275,6 +224,7 @@ public class TbDeviceStateNodeTest { @Test public void givenMetadataDoesNotContainTs_whenOnMsg_thenMsgTsIsUsedAsEventTs() { // GIVEN + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("1:1"); try { initNode(TbMsgType.ACTIVITY_EVENT); } catch (TbNodeException e) { @@ -298,9 +248,8 @@ public class TbDeviceStateNodeTest { @MethodSource public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { // GIVEN - given(ctxMock.getSelfId()).willReturn(nodeId); - given(ctxMock.newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING))).willReturn(cleanupMsg); given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("1:1"); given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); try { @@ -308,7 +257,6 @@ public class TbDeviceStateNodeTest { } catch (TbNodeException e) { fail("Node failed to initialize!", e); } - verifyCleanupMsgSent(); // WHEN node.onMsg(ctxMock, msg); @@ -345,10 +293,4 @@ public class TbDeviceStateNodeTest { node.init(ctxMock, nodeConfig); } - private void verifyCleanupMsgSent() { - then(ctxMock).should().getSelfId(); - then(ctxMock).should().newMsg(isNull(), eq(TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG), eq(nodeId), eq(TbMsgMetaData.EMPTY), eq(TbMsg.EMPTY_STRING)); - then(ctxMock).should().tellSelf(eq(cleanupMsg), eq(Duration.ofHours(1L).toMillis())); - } - } From 06bd88f7e9a4188459b5d1476685eb54c6280c27 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 17:28:07 +0200 Subject: [PATCH 35/39] Remove device state entries cleanup message --- .../java/org/thingsboard/server/common/data/msg/TbMsgType.java | 1 - .../org/thingsboard/server/common/data/msg/TbMsgTypeTest.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index b2014f4bb0..763f245a09 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -76,7 +76,6 @@ public enum TbMsgType { DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), DELAY_TIMEOUT_SELF_MSG(null, true), MSG_COUNT_SELF_MSG(null, true), - DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG(null, true), // Custom or N/A type: NA; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 529f6e6646..2221b236bd 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -26,7 +26,6 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIM import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG; -import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE; @@ -54,7 +53,6 @@ class TbMsgTypeTest { DEDUPLICATION_TIMEOUT_SELF_MSG, DELAY_TIMEOUT_SELF_MSG, MSG_COUNT_SELF_MSG, - DEVICE_STATE_STALE_ENTRIES_CLEANUP_SELF_MSG, NA ); From 89db4b1188477e2b61d645efaf61341bcfecefe0 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 17:37:28 +0200 Subject: [PATCH 36/39] Change proto numbers to avoid conflicts with PE --- common/proto/src/main/proto/queue.proto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 8c1918d5ef..149ac9c81f 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1295,9 +1295,9 @@ message ToCoreMsg { LifecycleEventProto lifecycleEventMsg = 8; ErrorEventProto errorEventMsg = 9; ToDeviceActorNotificationMsgProto toDeviceActorNotification = 10; - DeviceConnectProto deviceConnectMsg = 11; - DeviceDisconnectProto deviceDisconnectMsg = 12; - DeviceInactivityProto deviceInactivityMsg = 13; + DeviceConnectProto deviceConnectMsg = 13; + DeviceDisconnectProto deviceDisconnectMsg = 14; + DeviceInactivityProto deviceInactivityMsg = 15; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ From e586f367faa38044c4f6dee2958fb8bb022cf6e1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 18:05:01 +0200 Subject: [PATCH 37/39] Change log level to avoid cluttering logs --- .../service/state/DefaultRuleEngineDeviceStateManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java index 087f470fed..722fcd1aac 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java @@ -175,7 +175,7 @@ public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStat TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) { - log.info("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + log.debug("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); try { eventInfo.forwardToLocalService(); } catch (Exception e) { @@ -186,7 +186,7 @@ public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStat callback.onSuccess(); } else { TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg(); - log.info("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + log.debug("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); } } From 11cf5e7de899ca4ab44ce6633dceb04c07241255 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 18:36:16 +0200 Subject: [PATCH 38/39] Change proto numbering --- common/proto/src/main/proto/queue.proto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 149ac9c81f..e432b0dcec 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1295,9 +1295,9 @@ message ToCoreMsg { LifecycleEventProto lifecycleEventMsg = 8; ErrorEventProto errorEventMsg = 9; ToDeviceActorNotificationMsgProto toDeviceActorNotification = 10; - DeviceConnectProto deviceConnectMsg = 13; - DeviceDisconnectProto deviceDisconnectMsg = 14; - DeviceInactivityProto deviceInactivityMsg = 15; + DeviceConnectProto deviceConnectMsg = 50; + DeviceDisconnectProto deviceDisconnectMsg = 51; + DeviceInactivityProto deviceInactivityMsg = 52; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ From 73afd9a7f4e6190f1cc430a36af3c369e15dd8a3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Feb 2024 18:55:12 +0200 Subject: [PATCH 39/39] Make device state manage a rule engine component --- .../java/org/thingsboard/server/actors/ActorSystemContext.java | 2 +- .../service/state/DefaultRuleEngineDeviceStateManager.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 2a4f132be8..e3ebbdf892 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -204,7 +204,7 @@ public class ActorSystemContext { @Getter private DeviceCredentialsService deviceCredentialsService; - @Autowired + @Autowired(required = false) @Getter private RuleEngineDeviceStateManager deviceStateManager; diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java index 722fcd1aac..af2b03ec8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java @@ -29,12 +29,14 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.SimpleTbQueueCallback; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbRuleEngineComponent; import java.util.Optional; import java.util.UUID; @Slf4j @Service +@TbRuleEngineComponent public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { private final TbServiceInfoProvider serviceInfoProvider;