|
|
|
@ -13,10 +13,11 @@ |
|
|
|
* See the License for the specific language governing permissions and |
|
|
|
* limitations under the License. |
|
|
|
*/ |
|
|
|
package org.thingsboard.rule.engine.action; |
|
|
|
package org.thingsboard.rule.engine.transform; |
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.node.ArrayNode; |
|
|
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
|
|
|
import com.google.protobuf.ByteString; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.junit.jupiter.api.AfterEach; |
|
|
|
import org.junit.jupiter.api.Assertions; |
|
|
|
@ -27,6 +28,7 @@ import org.mockito.ArgumentMatchers; |
|
|
|
import org.mockito.stubbing.Answer; |
|
|
|
import org.thingsboard.common.util.JacksonUtil; |
|
|
|
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|
|
|
import org.thingsboard.rule.engine.api.RuleNodeCacheService; |
|
|
|
import org.thingsboard.rule.engine.api.TbContext; |
|
|
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|
|
|
import org.thingsboard.rule.engine.api.TbNodeException; |
|
|
|
@ -34,6 +36,7 @@ import org.thingsboard.rule.engine.api.TbRelationTypes; |
|
|
|
import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; |
|
|
|
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; |
|
|
|
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; |
|
|
|
import org.thingsboard.server.common.data.DataConstants; |
|
|
|
import org.thingsboard.server.common.data.id.DeviceId; |
|
|
|
import org.thingsboard.server.common.data.id.EntityId; |
|
|
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
|
|
|
@ -43,6 +46,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; |
|
|
|
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|
|
|
|
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Random; |
|
|
|
import java.util.UUID; |
|
|
|
@ -54,13 +58,18 @@ import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.concurrent.atomic.AtomicInteger; |
|
|
|
import java.util.concurrent.atomic.AtomicLong; |
|
|
|
import java.util.function.Consumer; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
import static org.mockito.ArgumentMatchers.anyList; |
|
|
|
import static org.mockito.ArgumentMatchers.anyLong; |
|
|
|
import static org.mockito.ArgumentMatchers.anyString; |
|
|
|
import static org.mockito.ArgumentMatchers.eq; |
|
|
|
import static org.mockito.ArgumentMatchers.isNull; |
|
|
|
import static org.mockito.ArgumentMatchers.nullable; |
|
|
|
import static org.mockito.Mockito.doAnswer; |
|
|
|
import static org.mockito.Mockito.mock; |
|
|
|
import static org.mockito.Mockito.never; |
|
|
|
import static org.mockito.Mockito.spy; |
|
|
|
import static org.mockito.Mockito.times; |
|
|
|
import static org.mockito.Mockito.verify; |
|
|
|
@ -69,13 +78,12 @@ import static org.mockito.Mockito.when; |
|
|
|
@Slf4j |
|
|
|
public class TbMsgDeduplicationNodeTest { |
|
|
|
|
|
|
|
private static final String MAIN_QUEUE_NAME = "Main"; |
|
|
|
private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority"; |
|
|
|
private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; |
|
|
|
private static final String DEDUPLICATION_IDS_CACHE_KEY = "deduplication_ids"; |
|
|
|
|
|
|
|
private TbContext ctx; |
|
|
|
|
|
|
|
private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); |
|
|
|
private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("deduplication-node-test"); |
|
|
|
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory); |
|
|
|
private final int deduplicationInterval = 1; |
|
|
|
|
|
|
|
@ -84,18 +92,24 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
private TbMsgDeduplicationNode node; |
|
|
|
private TbMsgDeduplicationNodeConfiguration config; |
|
|
|
private TbNodeConfiguration nodeConfiguration; |
|
|
|
private RuleNodeCacheService ruleNodeCacheService; |
|
|
|
|
|
|
|
private CountDownLatch awaitTellSelfLatch; |
|
|
|
|
|
|
|
@BeforeEach |
|
|
|
public void init() throws TbNodeException { |
|
|
|
ctx = mock(TbContext.class); |
|
|
|
ruleNodeCacheService = mock(RuleNodeCacheService.class); |
|
|
|
|
|
|
|
tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|
|
|
RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); |
|
|
|
|
|
|
|
when(ctx.getSelfId()).thenReturn(ruleNodeId); |
|
|
|
when(ctx.getTenantId()).thenReturn(tenantId); |
|
|
|
when(ctx.getRuleNodeCacheService()).thenReturn(ruleNodeCacheService); |
|
|
|
when(ruleNodeCacheService.getStrings(anyString())).thenReturn(Collections.emptySet()); |
|
|
|
when(ruleNodeCacheService.getEntityIds(anyString())).thenReturn(Collections.emptySet()); |
|
|
|
when(ruleNodeCacheService.getTbMsgs(anyString(), anyString())).thenReturn(Collections.emptySet()); |
|
|
|
|
|
|
|
doAnswer((Answer<TbMsg>) invocationOnMock -> { |
|
|
|
String type = (String) (invocationOnMock.getArguments())[1]; |
|
|
|
@ -171,11 +185,23 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|
|
|
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|
|
|
|
|
|
|
verify(ruleNodeCacheService, times(1)).getEntityIds(anyString()); |
|
|
|
verify(ruleNodeCacheService, times(1)).add(eq(DEDUPLICATION_IDS_CACHE_KEY), eq(deviceId)); |
|
|
|
verify(ruleNodeCacheService, times(msgCount)).add(eq(deviceId.toString()), any(TbMsg.class)); |
|
|
|
|
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); |
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|
|
|
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|
|
|
Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); |
|
|
|
|
|
|
|
for (Runnable valueCaptor : successCaptor.getAllValues()) { |
|
|
|
valueCaptor.run(); |
|
|
|
} |
|
|
|
verify(ruleNodeCacheService, times(1)).removeTbMsgList(eq(deviceId.toString()), anyList()); |
|
|
|
verify(ctx, never()).schedule(any(), anyLong(), any()); |
|
|
|
|
|
|
|
Assertions.assertEquals(TbMsg.toByteString(inputMsgs.get(0)), TbMsg.toByteString(newMsgCaptor.getValue())); |
|
|
|
} |
|
|
|
|
|
|
|
@Test |
|
|
|
@ -210,11 +236,23 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|
|
|
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|
|
|
|
|
|
|
verify(ruleNodeCacheService, times(1)).getEntityIds(anyString()); |
|
|
|
verify(ruleNodeCacheService, times(1)).add(eq(DEDUPLICATION_IDS_CACHE_KEY), eq(deviceId)); |
|
|
|
verify(ruleNodeCacheService, times(msgCount)).add(eq(deviceId.toString()), any(TbMsg.class)); |
|
|
|
|
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); |
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|
|
|
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|
|
|
Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); |
|
|
|
|
|
|
|
for (Runnable valueCaptor : successCaptor.getAllValues()) { |
|
|
|
valueCaptor.run(); |
|
|
|
} |
|
|
|
verify(ruleNodeCacheService, times(1)).removeTbMsgList(eq(deviceId.toString()), anyList()); |
|
|
|
verify(ctx, never()).schedule(any(), anyLong(), any()); |
|
|
|
|
|
|
|
Assertions.assertEquals(TbMsg.toByteString(msgWithLatestTs), TbMsg.toByteString(newMsgCaptor.getValue())); |
|
|
|
} |
|
|
|
|
|
|
|
@Test |
|
|
|
@ -227,7 +265,7 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
config.setInterval(deduplicationInterval); |
|
|
|
config.setStrategy(DeduplicationStrategy.ALL); |
|
|
|
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); |
|
|
|
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); |
|
|
|
config.setQueueName(DataConstants.HP_QUEUE_NAME); |
|
|
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|
|
|
node.init(ctx, nodeConfiguration); |
|
|
|
|
|
|
|
@ -245,10 +283,21 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|
|
|
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ruleNodeCacheService, times(1)).getEntityIds(anyString()); |
|
|
|
verify(ruleNodeCacheService, times(1)).add(eq(DEDUPLICATION_IDS_CACHE_KEY), eq(deviceId)); |
|
|
|
verify(ruleNodeCacheService, times(msgCount)).add(eq(deviceId.toString()), any(TbMsg.class)); |
|
|
|
|
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|
|
|
|
|
|
|
for (Runnable valueCaptor : successCaptor.getAllValues()) { |
|
|
|
valueCaptor.run(); |
|
|
|
} |
|
|
|
verify(ruleNodeCacheService, times(1)).removeTbMsgList(eq(deviceId.toString()), anyList()); |
|
|
|
verify(ctx, never()).schedule(any(), anyLong(), any()); |
|
|
|
|
|
|
|
Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); |
|
|
|
TbMsg outMessage = newMsgCaptor.getAllValues().get(0); |
|
|
|
Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData()); |
|
|
|
@ -267,7 +316,7 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
config.setInterval(deduplicationInterval); |
|
|
|
config.setStrategy(DeduplicationStrategy.ALL); |
|
|
|
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); |
|
|
|
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); |
|
|
|
config.setQueueName(DataConstants.HP_QUEUE_NAME); |
|
|
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|
|
|
node.init(ctx, nodeConfiguration); |
|
|
|
|
|
|
|
@ -278,7 +327,7 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
for (TbMsg msg : firstMsgPack) { |
|
|
|
node.onMsg(ctx, msg); |
|
|
|
} |
|
|
|
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); |
|
|
|
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); |
|
|
|
|
|
|
|
List<TbMsg> secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); |
|
|
|
for (TbMsg msg : secondMsgPack) { |
|
|
|
@ -291,10 +340,21 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|
|
|
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ruleNodeCacheService, times(1)).getEntityIds(anyString()); |
|
|
|
verify(ruleNodeCacheService, times(1)).add(eq(DEDUPLICATION_IDS_CACHE_KEY), eq(deviceId)); |
|
|
|
verify(ruleNodeCacheService, times(msgCount)).add(eq(deviceId.toString()), any(TbMsg.class)); |
|
|
|
|
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|
|
|
|
|
|
|
for (Runnable valueCaptor : successCaptor.getAllValues()) { |
|
|
|
valueCaptor.run(); |
|
|
|
} |
|
|
|
verify(ruleNodeCacheService, times(2)).removeTbMsgList(eq(deviceId.toString()), anyList()); |
|
|
|
verify(ctx, never()).schedule(any(), anyLong(), any()); |
|
|
|
|
|
|
|
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues(); |
|
|
|
Assertions.assertEquals(2, resultMsgs.size()); |
|
|
|
|
|
|
|
@ -345,14 +405,26 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|
|
|
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ruleNodeCacheService, times(1)).getEntityIds(anyString()); |
|
|
|
verify(ruleNodeCacheService, times(1)).add(eq(DEDUPLICATION_IDS_CACHE_KEY), eq(deviceId)); |
|
|
|
verify(ruleNodeCacheService, times(msgCount)).add(eq(deviceId.toString()), any(TbMsg.class)); |
|
|
|
|
|
|
|
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|
|
|
|
|
|
|
verify(ctx, times(msgCount)).ack(any()); |
|
|
|
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|
|
|
|
|
|
|
for (Runnable valueCaptor : successCaptor.getAllValues()) { |
|
|
|
valueCaptor.run(); |
|
|
|
} |
|
|
|
verify(ruleNodeCacheService, times(2)).removeTbMsgList(eq(deviceId.toString()), anyList()); |
|
|
|
verify(ctx, never()).schedule(any(), anyLong(), any()); |
|
|
|
|
|
|
|
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues(); |
|
|
|
Assertions.assertEquals(2, resultMsgs.size()); |
|
|
|
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); |
|
|
|
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); |
|
|
|
List<ByteString> resultMsgsByteStrings = resultMsgs.stream().map(TbMsg::toByteString).collect(Collectors.toList()); |
|
|
|
Assertions.assertTrue(resultMsgsByteStrings.contains(TbMsg.toByteString(msgWithLatestTsInFirstPack))); |
|
|
|
Assertions.assertTrue(resultMsgsByteStrings.contains(TbMsg.toByteString(msgWithLatestTsInSecondPack))); |
|
|
|
} |
|
|
|
|
|
|
|
private TbMsg getMsgWithLatestTs(List<TbMsg> firstMsgPack) { |
|
|
|
@ -381,7 +453,7 @@ public class TbMsgDeduplicationNodeTest { |
|
|
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
|
|
|
metaData.putValue("ts", String.valueOf(ts)); |
|
|
|
return TbMsg.newMsg( |
|
|
|
MAIN_QUEUE_NAME, |
|
|
|
DataConstants.MAIN_QUEUE_NAME, |
|
|
|
SessionMsgType.POST_TELEMETRY_REQUEST.name(), |
|
|
|
deviceId, |
|
|
|
metaData, |