committed by
GitHub
18 changed files with 599 additions and 68 deletions
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue