Browse Source

Merge pull request #14237 from dskarzh/rule-engine-consumer-loop-test

Add tests for rule engine consumer loop
pull/14999/head
Viacheslav Klimov 7 months ago
committed by GitHub
parent
commit
f68cc243e6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  2. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  3. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  5. 35
      application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  7. 12
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java
  10. 14
      application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java
  11. 10
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java
  13. 8
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  14. 3
      application/src/main/resources/thingsboard.yml
  15. 260
      application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java
  16. 281
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java
  17. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  18. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -147,15 +147,15 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception { private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception {
List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCalculatedFieldMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToCalculatedFieldMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1); CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToCalculatedFieldMsg>> ctx = new TbPackProcessingContext<>( TbPackProcessingContext<TbProtoQueueMsg<ToCalculatedFieldMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder<ToCalculatedFieldMsg> pendingMsgHolder = new PendingMsgHolder<>(); PendingMsgHolder<ToCalculatedFieldMsg> pendingMsgHolder = new PendingMsgHolder<>();
Future<?> packSubmitFuture = consumersExecutor.submit(() -> { Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
orderedMsgList.forEach((element) -> { orderedMsgList.forEach((element) -> {
UUID id = element.getUuid(); UUID id = element.uuid();
TbProtoQueueMsg<ToCalculatedFieldMsg> msg = element.getMsg(); TbProtoQueueMsg<ToCalculatedFieldMsg> msg = element.msg();
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx); TbCallback callback = new TbPackCallback<>(id, ctx);
try { try {

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -260,15 +260,15 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception { private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception {
List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1); CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>( TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder<ToCoreMsg> pendingMsgHolder = new PendingMsgHolder<>(); PendingMsgHolder<ToCoreMsg> pendingMsgHolder = new PendingMsgHolder<>();
Future<?> packSubmitFuture = consumersExecutor.submit(() -> { Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
orderedMsgList.forEach((element) -> { orderedMsgList.forEach((element) -> {
UUID id = element.getUuid(); UUID id = element.uuid();
TbProtoQueueMsg<ToCoreMsg> msg = element.getMsg(); TbProtoQueueMsg<ToCoreMsg> msg = element.msg();
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx); TbCallback callback = new TbPackCallback<>(id, ctx);
try { try {

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -128,15 +128,15 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, Object consumerKey, QueueConfig edgeQueueConfig) throws InterruptedException { private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, Object consumerKey, QueueConfig edgeQueueConfig) throws InterruptedException {
List<IdMsgPair<ToEdgeMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToEdgeMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToEdgeMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToEdgeMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1); CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToEdgeMsg>> ctx = new TbPackProcessingContext<>( TbPackProcessingContext<TbProtoQueueMsg<ToEdgeMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder<ToEdgeMsg> pendingMsgHolder = new PendingMsgHolder<>(); PendingMsgHolder<ToEdgeMsg> pendingMsgHolder = new PendingMsgHolder<>();
Future<?> submitFuture = consumersExecutor.submit(() -> { Future<?> submitFuture = consumersExecutor.submit(() -> {
orderedMsgList.forEach((element) -> { orderedMsgList.forEach((element) -> {
UUID id = element.getUuid(); UUID id = element.uuid();
TbProtoQueueMsg<ToEdgeMsg> msg = element.getMsg(); TbProtoQueueMsg<ToEdgeMsg> msg = element.msg();
TbCallback callback = new TbPackCallback<>(id, ctx); TbCallback callback = new TbPackCallback<>(id, ctx);
try { try {
ToEdgeMsg toEdgeMsg = msg.getValue(); ToEdgeMsg toEdgeMsg = msg.getValue();

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -70,6 +70,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
private final TbRuleEngineConsumerContext ctx; private final TbRuleEngineConsumerContext ctx;
private final QueueService queueService; private final QueueService queueService;
private final TbRuleEngineDeviceRpcService tbDeviceRpcService; private final TbRuleEngineDeviceRpcService tbDeviceRpcService;
private final TbMsgPackProcessingContextFactory packProcessingContextFactory;
private final ConcurrentMap<QueueKey, TbRuleEngineQueueConsumerManager> consumers = new ConcurrentHashMap<>(); private final ConcurrentMap<QueueKey, TbRuleEngineQueueConsumerManager> consumers = new ConcurrentHashMap<>();
@ -85,11 +86,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService, JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) { CalculatedFieldCache calculatedFieldCache,
TbMsgPackProcessingContextFactory packProcessingContextFactory) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx; this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService; this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService; this.queueService = queueService;
this.packProcessingContextFactory = packProcessingContextFactory;
} }
@Override @Override
@ -255,6 +258,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
.consumerExecutor(consumersExecutor) .consumerExecutor(consumersExecutor)
.scheduler(scheduler) .scheduler(scheduler)
.taskExecutor(mgmtExecutor) .taskExecutor(mgmtExecutor)
.packProcessingContextFactory(packProcessingContextFactory)
.build(); .build();
consumers.put(queueKey, consumer); consumers.put(queueKey, consumer);
consumer.init(queue); consumer.init(queue);

35
application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2025 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.queue;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
public interface TbMsgPackProcessingContextFactory {
TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts);
@Component
class DefaultTbMsgPackProcessingContextFactory implements TbMsgPackProcessingContextFactory {
@Override
public TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts) {
return new TbMsgPackProcessingContext(queueName, submitStrategy, skipTimeouts);
}
}
}

6
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -134,13 +134,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected void processNotifications(List<TbProtoQueueMsg<N>> msgs, TbQueueConsumer<TbProtoQueueMsg<N>> consumer) throws Exception { protected void processNotifications(List<TbProtoQueueMsg<N>> msgs, TbQueueConsumer<TbProtoQueueMsg<N>> consumer) throws Exception {
List<IdMsgPair<N>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<N>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1); CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>( TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
orderedMsgList.forEach(element -> { orderedMsgList.forEach(element -> {
UUID id = element.getUuid(); UUID id = element.uuid();
TbProtoQueueMsg<N> msg = element.getMsg(); TbProtoQueueMsg<N> msg = element.msg();
log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx); TbCallback callback = new TbPackCallback<>(id, ctx);
try { try {

12
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java

@ -44,21 +44,21 @@ public abstract class AbstractTbRuleEngineSubmitStrategy implements TbRuleEngine
@Override @Override
public ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> getPendingMap() { public ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> getPendingMap() {
return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid, pair -> pair.msg)); return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid(), pair -> pair.msg()));
} }
@Override @Override
public void update(ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> reprocessMap) { public void update(ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> reprocessMap) {
List<IdMsgPair<TransportProtos.ToRuleEngineMsg>> newOrderedMsgList = new ArrayList<>(reprocessMap.size()); List<IdMsgPair<TransportProtos.ToRuleEngineMsg>> newOrderedMsgList = new ArrayList<>(reprocessMap.size());
for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) { for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) {
if (reprocessMap.containsKey(pair.uuid)) { if (reprocessMap.containsKey(pair.uuid())) {
if (StringUtils.isNotEmpty(pair.getMsg().getValue().getFailureMessage())) { if (StringUtils.isNotEmpty(pair.msg().getValue().getFailureMessage())) {
var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.getMsg().getValue()) var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.msg().getValue())
.clearFailureMessage() .clearFailureMessage()
.clearRelationTypes() .clearRelationTypes()
.build(); .build();
var newMsg = new TbProtoQueueMsg<>(pair.getMsg().getKey(), toRuleEngineMsg, pair.getMsg().getHeaders()); var newMsg = new TbProtoQueueMsg<>(pair.msg().getKey(), toRuleEngineMsg, pair.msg().getHeaders());
newOrderedMsgList.add(new IdMsgPair<>(pair.getUuid(), newMsg)); newOrderedMsgList.add(new IdMsgPair<>(pair.uuid(), newMsg));
} else { } else {
newOrderedMsgList.add(pair); newOrderedMsgList.add(pair);
} }

2
application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java

@ -73,7 +73,7 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS
pendingPack.clear(); pendingPack.clear();
for (int i = startIdx; i < endIdx; i++) { for (int i = startIdx; i < endIdx; i++) {
IdMsgPair<TransportProtos.ToRuleEngineMsg> pair = orderedMsgList.get(i); IdMsgPair<TransportProtos.ToRuleEngineMsg> pair = orderedMsgList.get(i);
pendingPack.put(pair.uuid, pair.msg); pendingPack.put(pair.uuid(), pair.msg());
} }
tmpPack = new LinkedHashMap<>(pendingPack); tmpPack = new LinkedHashMap<>(pendingPack);
} }

2
application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java

@ -34,7 +34,7 @@ public class BurstTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size()); log.debug("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size());
} }
orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid, pair.msg)); orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid(), pair.msg()));
} }
@Override @Override

14
application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java

@ -15,19 +15,9 @@
*/ */
package org.thingsboard.server.service.queue.processing; package org.thingsboard.server.service.queue.processing;
import lombok.Getter; import com.google.protobuf.GeneratedMessageV3;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.util.UUID; import java.util.UUID;
public class IdMsgPair<T extends com.google.protobuf.GeneratedMessageV3> { public record IdMsgPair<T extends GeneratedMessageV3>(UUID uuid, TbProtoQueueMsg<T> msg) {}
@Getter
final UUID uuid;
@Getter
final TbProtoQueueMsg<T> msg;
public IdMsgPair(UUID uuid, TbProtoQueueMsg<T> msg) {
this.uuid = uuid;
this.msg = msg;
}
}

10
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java

@ -51,7 +51,7 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs
entityIdToListMap.forEach((entityId, queue) -> { entityIdToListMap.forEach((entityId, queue) -> {
IdMsgPair<TransportProtos.ToRuleEngineMsg> msg = queue.peek(); IdMsgPair<TransportProtos.ToRuleEngineMsg> msg = queue.peek();
if (msg != null) { if (msg != null) {
msgConsumer.accept(msg.uuid, msg.msg); msgConsumer.accept(msg.uuid(), msg.msg());
} }
}); });
} }
@ -71,13 +71,13 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs
IdMsgPair<TransportProtos.ToRuleEngineMsg> next = null; IdMsgPair<TransportProtos.ToRuleEngineMsg> next = null;
synchronized (queue) { synchronized (queue) {
IdMsgPair<TransportProtos.ToRuleEngineMsg> expected = queue.peek(); IdMsgPair<TransportProtos.ToRuleEngineMsg> expected = queue.peek();
if (expected != null && expected.uuid.equals(id)) { if (expected != null && expected.uuid().equals(id)) {
queue.poll(); queue.poll();
next = queue.peek(); next = queue.peek();
} }
} }
if (next != null) { if (next != null) {
msgConsumer.accept(next.uuid, next.msg); msgConsumer.accept(next.uuid(), next.msg());
} }
} }
} }
@ -87,9 +87,9 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs
msgToEntityIdMap.clear(); msgToEntityIdMap.clear();
entityIdToListMap.clear(); entityIdToListMap.clear();
for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) { for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) {
EntityId entityId = getEntityId(pair.msg.getValue()); EntityId entityId = getEntityId(pair.msg().getValue());
if (entityId != null) { if (entityId != null) {
msgToEntityIdMap.put(pair.uuid, entityId); msgToEntityIdMap.put(pair.uuid(), entityId);
entityIdToListMap.computeIfAbsent(entityId, id -> new LinkedList<>()).add(pair); entityIdToListMap.computeIfAbsent(entityId, id -> new LinkedList<>()).add(pair);
} }
} }

6
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java

@ -60,11 +60,11 @@ public class SequentialTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSu
int idx = msgIdx.get(); int idx = msgIdx.get();
if (idx < listSize) { if (idx < listSize) {
IdMsgPair<TransportProtos.ToRuleEngineMsg> pair = orderedMsgList.get(idx); IdMsgPair<TransportProtos.ToRuleEngineMsg> pair = orderedMsgList.get(idx);
expectedMsgId = pair.uuid; expectedMsgId = pair.uuid();
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg); log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg());
} }
msgConsumer.accept(pair.uuid, pair.msg); msgConsumer.accept(pair.uuid(), pair.msg());
} }
} }

8
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java

@ -42,6 +42,7 @@ import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.Consumer
import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackCallback;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
@ -67,13 +68,15 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
private final TbRuleEngineConsumerContext ctx; private final TbRuleEngineConsumerContext ctx;
private final TbRuleEngineConsumerStats stats; private final TbRuleEngineConsumerStats stats;
private final TbMsgPackProcessingContextFactory packProcessingContextFactory;
@Builder(builderMethodName = "create") // not to conflict with super.builder() @Builder(builderMethodName = "create") // not to conflict with super.builder()
public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx,
QueueKey queueKey, QueueKey queueKey,
ExecutorService consumerExecutor, ExecutorService consumerExecutor,
ScheduledExecutorService scheduler, ScheduledExecutorService scheduler,
ExecutorService taskExecutor) { ExecutorService taskExecutor,
TbMsgPackProcessingContextFactory packProcessingContextFactory) {
super(queueKey, null, null, super(queueKey, null, null,
(queueConfig, tpi) -> { (queueConfig, tpi) -> {
Integer partitionId = tpi != null ? tpi.getPartition().orElse(-1) : null; Integer partitionId = tpi != null ? tpi.getPartition().orElse(-1) : null;
@ -82,6 +85,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
consumerExecutor, scheduler, taskExecutor, null); consumerExecutor, scheduler, taskExecutor, null);
this.ctx = ctx; this.ctx = ctx;
this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory()); this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory());
this.packProcessingContextFactory = packProcessingContextFactory;
} }
public void delete(boolean drainQueue) { public void delete(boolean drainQueue) {
@ -134,7 +138,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue);
submitStrategy.init(msgs); submitStrategy.init(msgs);
while (!stopped && !consumer.isStopped()) { while (!stopped && !consumer.isStopped()) {
TbMsgPackProcessingContext packCtx = new TbMsgPackProcessingContext(queue.getName(), submitStrategy, ackStrategy.isSkipTimeoutMsgs()); TbMsgPackProcessingContext packCtx = packProcessingContextFactory.create(queue.getName(), submitStrategy, ackStrategy.isSkipTimeoutMsgs());
submitStrategy.submitAttempt((id, msg) -> submitMessage(packCtx, id, msg)); submitStrategy.submitAttempt((id, msg) -> submitMessage(packCtx, id, msg));
final boolean timeout = !packCtx.await(queue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); final boolean timeout = !packCtx.await(queue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS);

3
application/src/main/resources/thingsboard.yml

@ -1910,6 +1910,9 @@ queue:
print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}"
# Max length of the error message that is printed by statistics # Max length of the error message that is printed by statistics
max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}"
prometheus-stats:
# Enable/disable Prometheus statistics for individual Rule Engine message processing (records time in ms for success/failure).
enabled: "${TB_QUEUE_RULE_ENGINE_PROMETHEUS_STATS_ENABLED:false}"
# After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics
topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}"
# Size of the thread pool that handles such operations as partition changes, config updates, queue deletion # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion

260
application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java

@ -15,14 +15,17 @@
*/ */
package org.thingsboard.server.service.queue; package org.thingsboard.server.service.queue;
import lombok.extern.slf4j.Slf4j; import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After; import org.junit.jupiter.api.AfterEach;
import org.junit.Assert; import org.junit.jupiter.api.BeforeEach;
import org.junit.Test; import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
@ -35,30 +38,241 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertTrue; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@Slf4j @ExtendWith(MockitoExtension.class)
@RunWith(MockitoJUnitRunner.class) class TbMsgPackProcessingContextTest {
public class TbMsgPackProcessingContextTest {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
@Mock
TbRuleEngineSubmitStrategy submitStrategy;
@Mock
TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg> mockMsg;
ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> pendingMap;
public static final int TIMEOUT = 10;
ExecutorService executorService; ExecutorService executorService;
@After @BeforeEach
public void tearDown() { void setup() {
pendingMap = new ConcurrentHashMap<>();
lenient().when(submitStrategy.getPendingMap()).thenReturn(pendingMap);
}
@AfterEach
void tearDown() {
if (executorService != null) { if (executorService != null) {
executorService.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(executorService, 5, TimeUnit.SECONDS);
} }
} }
@Test @Test
public void testHighConcurrencyCase() throws InterruptedException { void testAwait_shouldReturnTrue_whenOnSuccessIsCalledBeforeTimeout() throws InterruptedException {
//log.warn("preparing the test..."); // GIVEN - a context with one pending message
executorService = Executors.newSingleThreadExecutor();
UUID msgId = UUID.randomUUID();
pendingMap.put(msgId, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
// WHEN - onSuccess() is called in another thread before timeout
executorService.submit(() -> {
try {
Thread.sleep(100);
context.onSuccess(msgId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// THEN - await() should return true (successful completion)
boolean result = context.await(5000, TimeUnit.MILLISECONDS);
assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue();
// Verify the message was moved to success map
assertThat(context.getSuccessMap()).containsKey(msgId);
assertThat(context.getPendingMap()).isEmpty();
assertThat(context.getExceptionsMap()).isEmpty();
// Verify submit strategy was notified about successful message processing
then(submitStrategy).should().onSuccess(msgId);
}
@Test
void testAwait_shouldReturnTrue_whenOnFailureIsCalledBeforeTimeout() throws InterruptedException {
// GIVEN - a context with one pending message
executorService = Executors.newSingleThreadExecutor();
UUID msgId = UUID.randomUUID();
pendingMap.put(msgId, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
var exception = new RuleEngineException("Test exception");
// WHEN - onFailure() is called in another thread before timeout
executorService.submit(() -> {
try {
Thread.sleep(100);
context.onFailure(tenantId, msgId, exception);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// THEN - await() should return true (successful completion, even if message processing failed)
boolean result = context.await(5000, TimeUnit.MILLISECONDS);
assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue();
// Verify the exception was added to exceptions map
assertThat(context.getSuccessMap()).isEmpty();
assertThat(context.getPendingMap()).isEmpty();
assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception);
}
@Test
void testAwait_shouldReturnFalse_whenTimeoutOccurs() throws InterruptedException {
// GIVEN - a context with one pending message and no processing
UUID msgId = UUID.randomUUID();
pendingMap.put(msgId, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
// WHEN - await() is called with short timeout and no message processing happens
long startTime = System.nanoTime();
boolean result = context.await(100, TimeUnit.MILLISECONDS);
long elapsedTime = System.nanoTime() - startTime;
// THEN - await() should return false (timeout occurred)
assertThat(result).as("await() should return false when timeout occurs").isFalse();
assertThat(elapsedTime).as("await() should wait for at least the timeout duration").isGreaterThanOrEqualTo(100L);
// Message should still be in pending map
assertThat(context.getSuccessMap()).isEmpty();
assertThat(context.getPendingMap()).containsKey(msgId);
assertThat(context.getExceptionsMap()).isEmpty();
}
@Test
void testAwait_shouldHandleMultiplePendingMessages() throws InterruptedException {
// GIVEN - a context with multiple pending messages
executorService = Executors.newSingleThreadExecutor();
UUID msgId1 = UUID.randomUUID();
UUID msgId2 = UUID.randomUUID();
UUID msgId3 = UUID.randomUUID();
pendingMap.put(msgId1, mockMsg);
pendingMap.put(msgId2, mockMsg);
pendingMap.put(msgId3, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
// WHEN - messages are processed one by one
executorService.submit(() -> {
try {
Thread.sleep(50);
context.onSuccess(msgId1);
Thread.sleep(50);
context.onSuccess(msgId2);
Thread.sleep(50);
context.onSuccess(msgId3);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// THEN - await() should return true only after all messages are processed
boolean result = context.await(5000, TimeUnit.MILLISECONDS);
assertThat(result).as("await() should return true after all messages are processed").isTrue();
// All messages should be in success map
assertThat(context.getSuccessMap()).containsKeys(msgId1, msgId2, msgId3);
assertThat(context.getPendingMap()).isEmpty();
assertThat(context.getExceptionsMap()).isEmpty();
}
@Test
void testAwait_shouldNotCountDownPrematurely_withMultipleMessages() throws InterruptedException {
// GIVEN - a context with multiple pending messages
executorService = Executors.newSingleThreadExecutor();
UUID msgId1 = UUID.randomUUID();
UUID msgId2 = UUID.randomUUID();
pendingMap.put(msgId1, mockMsg);
pendingMap.put(msgId2, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
// WHEN - only one message is processed
executorService.submit(() -> {
try {
Thread.sleep(100);
context.onSuccess(msgId1);
// msgId2 still in processing
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// THEN: await should timeout because not all messages were processed
boolean result = context.await(2000, TimeUnit.MILLISECONDS);
assertThat(result).as("await() should timeout when not all messages are processed").isFalse();
// One message in success, one still pending
assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1);
assertThat(context.getPendingMap()).containsOnlyKeys(msgId2);
assertThat(context.getExceptionsMap()).isEmpty();
}
@Test
void testAwait_shouldHandleMixedSuccessAndFailure() throws InterruptedException {
// GIVEN - multiple messages
executorService = Executors.newSingleThreadExecutor();
UUID msgId1 = UUID.randomUUID();
UUID msgId2 = UUID.randomUUID();
pendingMap.put(msgId1, mockMsg);
pendingMap.put(msgId2, mockMsg);
var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false);
var exception = new RuleEngineException("Test exception");
// WHEN - one succeeds, one fails
executorService.submit(() -> {
try {
Thread.sleep(50);
context.onSuccess(msgId1);
Thread.sleep(50);
context.onFailure(tenantId, msgId2, exception);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// THEN - await() should complete successfully
boolean result = context.await(5000, TimeUnit.MILLISECONDS);
assertThat(result).as("await() should return true when all messages are processed").isTrue();
assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1);
assertThat(context.getPendingMap()).isEmpty();
assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception);
}
@Test
void testHighConcurrencyCase() throws InterruptedException {
int msgCount = 1000; int msgCount = 1000;
int parallelCount = 5; int parallelCount = 5;
executorService = Executors.newFixedThreadPool(parallelCount, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")); executorService = Executors.newFixedThreadPool(parallelCount, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"));
@ -76,28 +290,24 @@ public class TbMsgPackProcessingContextTest {
final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch finishLatch = new CountDownLatch(parallelCount); final CountDownLatch finishLatch = new CountDownLatch(parallelCount);
for (int i = 0; i < parallelCount; i++) { for (int i = 0; i < parallelCount; i++) {
//final String taskName = "" + uuid + " " + i;
executorService.submit(() -> { executorService.submit(() -> {
//log.warn("ready {}", taskName);
readyLatch.countDown(); readyLatch.countDown();
try { try {
startLatch.await(); startLatch.await();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Assert.fail("failed to await"); fail("failed to await");
} }
//log.warn("go {}", taskName);
context.onSuccess(uuid); context.onSuccess(uuid);
finishLatch.countDown(); finishLatch.countDown();
}); });
} }
assertTrue(readyLatch.await(TIMEOUT, TimeUnit.SECONDS)); assertTrue(readyLatch.await(10, TimeUnit.SECONDS));
Thread.yield(); Thread.yield();
startLatch.countDown(); //run all-at-once submitted tasks startLatch.countDown(); //run all-at-once submitted tasks
assertTrue(finishLatch.await(TIMEOUT, TimeUnit.SECONDS)); assertTrue(finishLatch.await(10, TimeUnit.SECONDS));
} }
assertTrue(context.await(TIMEOUT, TimeUnit.SECONDS)); assertTrue(context.await(10, TimeUnit.SECONDS));
verify(strategyMock, times(msgCount)).onSuccess(any(UUID.class)); verify(strategyMock, times(msgCount)).onSuccess(any(UUID.class));
} }
} }

281
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java

@ -0,0 +1,281 @@
/**
* Copyright © 2016-2025 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.queue.ruleengine;
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;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
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.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.DefaultTbQueueMsgHeaders;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.memory.DefaultInMemoryStorage;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContext;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RuleEngineConsumerLoopTest {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
InMemoryStorage storage;
@Mock
ActorSystemContext actorContext;
@Mock
StatsFactory statsFactory;
@Mock
TbRuleEngineQueueFactory queueFactory;
@Mock
RuleEngineStatisticsService statisticsService;
@Mock
TbServiceInfoProvider serviceInfoProvider;
@Mock
PartitionService partitionService;
@Mock
TbQueueProducerProvider producerProvider;
@Mock
TbQueueAdmin queueAdmin;
@Mock
TbMsgPackProcessingContextFactory packProcessingContextFactory;
@Mock
TbMsgPackProcessingContext packCtx;
Queue mainQueue;
TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> consumer;
TbRuleEngineConsumerContext ruleEngineConsumerContext;
TbRuleEngineQueueConsumerManager consumerManager;
ExecutorService consumersExecutor;
ScheduledExecutorService scheduler;
ExecutorService mgmtExecutor;
@BeforeEach
void setup() throws InterruptedException {
consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer"));
scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("tb-rule-engine-consumer-scheduler");
mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(1, "tb-rule-engine-mgmt");
mainQueue = new Queue();
mainQueue.setTenantId(TenantId.SYS_TENANT_ID);
mainQueue.setName("Main");
mainQueue.setTopic("tb_rule_engine.main");
mainQueue.setPollInterval(25);
mainQueue.setPartitions(1);
mainQueue.setConsumerPerPartition(false);
mainQueue.setPackProcessingTimeout(2000L);
var submitStrategy = new SubmitStrategy();
submitStrategy.setType(SubmitStrategyType.BURST);
submitStrategy.setBatchSize(1000);
mainQueue.setSubmitStrategy(submitStrategy);
var processingStrategy = new ProcessingStrategy();
processingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES);
processingStrategy.setRetries(3);
processingStrategy.setFailurePercentage(0.0);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(3);
mainQueue.setProcessingStrategy(processingStrategy);
storage = new DefaultInMemoryStorage();
consumer = spy(new InMemoryTbQueueConsumer<>(storage, mainQueue.getTopic()));
given(queueFactory.createToRuleEngineMsgConsumer(eq(mainQueue), isNull())).willReturn(consumer);
ruleEngineConsumerContext = new TbRuleEngineConsumerContext(
actorContext, statsFactory, new TbRuleEngineSubmitStrategyFactory(), new TbRuleEngineProcessingStrategyFactory(),
queueFactory, statisticsService, serviceInfoProvider, partitionService, producerProvider, queueAdmin
);
ruleEngineConsumerContext.setPollDuration(25);
ruleEngineConsumerContext.setPackProcessingTimeout(2000);
ruleEngineConsumerContext.setStatsEnabled(false); // true by default
ruleEngineConsumerContext.setPrometheusStatsEnabled(false);
ruleEngineConsumerContext.setTopicDeletionDelayInSec(15);
ruleEngineConsumerContext.setMgmtThreadPoolSize(12);
// Tell the (mock) context factory to return (mock) message pack context
given(packProcessingContextFactory.create(
eq(mainQueue.getName()),
any(TbRuleEngineSubmitStrategy.class),
eq(false)
)).willAnswer(invocation -> {
TbRuleEngineSubmitStrategy realStrategy = invocation.getArgument(1);
when(packCtx.getPendingMap()).thenAnswer(i -> realStrategy.getPendingMap());
when(packCtx.getFailedMap()).thenReturn(new ConcurrentHashMap<>());
return packCtx;
});
// Tell the (mock) context's await() to return 'false' (always timeout) immediately
given(packCtx.await(anyLong(), any(TimeUnit.class))).willReturn(false);
consumerManager = TbRuleEngineQueueConsumerManager.create()
.ctx(ruleEngineConsumerContext)
.queueKey(new QueueKey(ServiceType.TB_RULE_ENGINE, mainQueue))
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)
.packProcessingContextFactory(packProcessingContextFactory)
.build();
}
@AfterEach
void destroy() {
MoreExecutors.shutdownAndAwaitTermination(scheduler, Duration.ofSeconds(30));
MoreExecutors.shutdownAndAwaitTermination(mgmtExecutor, Duration.ofSeconds(30));
MoreExecutors.shutdownAndAwaitTermination(consumersExecutor, Duration.ofSeconds(30));
}
@Test
void consumerLoopTest_verifyOperationsOrder() throws InterruptedException {
// Create partition
var partition = TopicPartitionInfo.builder()
.tenantId(TenantId.SYS_TENANT_ID)
.topic(mainQueue.getTopic())
.partition(0)
.myPartition(true)
.useInternalPartition(false)
.build();
// Put 10k messages to the queue
for (int i = 0; i < 10_000; i++) {
var tbMsg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(deviceId)
.data("{\"temperature\":123}")
.metaData(TbMsgMetaData.EMPTY)
.build();
var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTbMsgProto(TbMsg.toProto(tbMsg))
.addAllRelationTypes(Set.of("Success"))
.build();
storage.put(partition.getFullTopicName(), new TbProtoQueueMsg<>(UUID.randomUUID(), toRuleEngineMsg, new DefaultTbQueueMsgHeaders()));
}
// Count how many polls were made
var totalPolls = new AtomicInteger(0);
var emptyPolls = new AtomicInteger(0);
doAnswer(invocation -> {
totalPolls.incrementAndGet();
@SuppressWarnings("unchecked")
var messages = (List<TbQueueMsg>) invocation.callRealMethod();
if (messages.isEmpty()) {
emptyPolls.incrementAndGet();
}
return messages;
}).when(consumer).poll(mainQueue.getPollInterval());
// Count how many commits were made
var totalCommits = new AtomicInteger(0);
doAnswer(invocation -> {
totalCommits.incrementAndGet();
return invocation.callRealMethod();
}).when(consumer).commit();
// Initialize consumer
consumerManager.init(mainQueue);
// Assign partition to the consumer
consumerManager.update(Set.of(partition));
// Give some time for the consumer to get all messages
await().atMost(Duration.ofSeconds(10L)).until(() -> storage.getLagTotal() == 0);
// Stop consumer
consumerManager.stop();
consumerManager.awaitStop();
// Determine number of non-empty consumer iterations made, since polling does not stop immediately after consuming all messages and may do a few empty polls
int nonEmptyPolls = totalPolls.get() - emptyPolls.get();
// Verify that there is 10 polls and 10 matching commits
// Each poll consumes 1k messages and queue has 10k total, so that means 10k total msgs / 1k msgs per poll = 10 polls
assertThat(nonEmptyPolls).isEqualTo(10).isEqualTo(totalCommits.get());
// Verify that poll-await-commit cycle happened in order with correct await timeout
InOrder inOrder = inOrder(consumer, packCtx);
for (int i = 0; i < nonEmptyPolls; i++) {
inOrder.verify(consumer).poll(mainQueue.getPollInterval());
inOrder.verify(packCtx).await(mainQueue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS);
inOrder.verify(consumer).commit();
}
}
}

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -59,6 +59,7 @@ import org.thingsboard.server.queue.provider.KafkaMonolithQueueFactory;
import org.thingsboard.server.queue.provider.KafkaTbRuleEngineQueueFactory; import org.thingsboard.server.queue.provider.KafkaTbRuleEngineQueueFactory;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService; import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
@ -194,6 +195,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
.consumerExecutor(consumersExecutor) .consumerExecutor(consumersExecutor)
.scheduler(scheduler) .scheduler(scheduler)
.taskExecutor(mgmtExecutor) .taskExecutor(mgmtExecutor)
.packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory())
.build(); .build();
} }

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java

@ -45,6 +45,7 @@ import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey;
import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
@ -196,6 +197,7 @@ public class TbRuleEngineStrategyTest {
var consumerManager = TbRuleEngineQueueConsumerManager.create() var consumerManager = TbRuleEngineQueueConsumerManager.create()
.ctx(ruleEngineConsumerContext) .ctx(ruleEngineConsumerContext)
.queueKey(queueKey) .queueKey(queueKey)
.packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory())
.build(); .build();
consumerManager.init(queue); consumerManager.init(queue);

Loading…
Cancel
Save