From 215c5dbb966f8783271a5fa6be56073b64d76524 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 23 Apr 2025 15:44:37 +0300 Subject: [PATCH 01/44] Jobs --- .../job/CfReprocessingJobProcessor.java | 83 +++++++++++ .../server/service/job/DefaultJobManager.java | 136 +++++++++++++++++ .../server/service/job/JobManager.java | 24 +++ .../server/service/job/JobProcessor.java | 30 ++++ .../job/task/CfReprocessingTaskProcessor.java | 57 +++++++ .../src/main/resources/thingsboard.yml | 2 + .../src/test/resources/logback-test.xml | 2 +- .../server/dao/task/JobService.java | 35 +++++ .../server/common/data/EntityType.java | 3 +- .../common/data/id/EntityIdFactory.java | 2 + .../server/common/data/id/JobId.java | 38 +++++ .../job/CfReprocessingJobConfiguration.java | 39 +++++ .../data/job/CfReprocessingJobResult.java | 25 ++++ .../common/data/job/CfReprocessingTask.java | 53 +++++++ .../server/common/data/job/Job.java | 56 +++++++ .../common/data/job/JobConfiguration.java | 32 ++++ .../server/common/data/job/JobResult.java | 41 ++++++ .../server/common/data/job/JobStatus.java | 24 +++ .../server/common/data/job/JobType.java | 26 ++++ .../server/common/data/job/Task.java | 50 +++++++ .../server/common/data/job/TaskResult.java | 45 ++++++ common/proto/src/main/proto/queue.proto | 8 + .../queue/kafka/TbKafkaTopicConfigs.java | 5 + .../InMemoryMonolithQueueFactory.java | 22 ++- .../provider/KafkaMonolithQueueFactory.java | 51 +++++++ .../provider/KafkaTbCoreQueueFactory.java | 28 ++++ .../KafkaTbRuleEngineQueueFactory.java | 29 ++++ .../provider/TaskProcessorQueueFactory.java | 31 ++++ .../queue/provider/TbCoreQueueFactory.java | 7 + .../provider/TbRuleEngineQueueFactory.java | 2 +- .../server/queue/task/TaskProcessor.java | 139 ++++++++++++++++++ .../server/dao/model/ModelConstants.java | 10 ++ .../server/dao/model/sql/JobEntity.java | 96 ++++++++++++ .../server/dao/sql/task/JobRepository.java | 69 +++++++++ .../server/dao/sql/task/JpaJobDao.java | 72 +++++++++ .../server/dao/task/DefaultJobService.java | 86 +++++++++++ .../thingsboard/server/dao/task/JobDao.java | 33 +++++ .../main/resources/sql/schema-entities.sql | 11 ++ 38 files changed, 1498 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/JobManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java diff --git a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java new file mode 100644 index 0000000000..3b63d3736f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java @@ -0,0 +1,83 @@ +/** + * 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.job; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ProfileEntityIdInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.job.CfReprocessingJobConfiguration; +import org.thingsboard.server.common.data.job.CfReprocessingTask; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +public class CfReprocessingJobProcessor extends JobProcessor { + + private final DeviceService deviceService; + private final AssetService assetService; + + @Override + public void process(Job job, Consumer taskConsumer) { + CfReprocessingJobConfiguration configuration = job.getConfiguration(); + + CalculatedField calculatedField = configuration.getCalculatedField(); + EntityId entityId = calculatedField.getEntityId(); + + if (entityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { + taskConsumer.accept(createTask(job, configuration, entityId)); + } else { + PageDataIterable entities; + if (entityId.getEntityType() == EntityType.DEVICE_PROFILE) { + entities = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(job.getTenantId(), pageLink), 512); + } else if (entityId.getEntityType() == EntityType.ASSET_PROFILE) { + entities = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(job.getTenantId(), pageLink), 512); + } else { + throw new IllegalArgumentException("Unsupported CF entity type " + entityId.getEntityType()); + } + entities.forEach(device -> { + taskConsumer.accept(createTask(job, configuration, device.getEntityId())); + }); + } + } + + private Task createTask(Job job, CfReprocessingJobConfiguration configuration, EntityId entityId) { + return CfReprocessingTask.builder() + .tenantId(job.getTenantId()) + .jobId(job.getId()) + .key(entityId.getEntityType().getNormalName() + " " + entityId.getId()) + .calculatedField(configuration.getCalculatedField()) + .entityId(entityId) + .startTs(configuration.getStartTs()) + .endTs(configuration.getEndTs()) + .build(); + } + + @Override + public JobType getType() { + return JobType.CF_REPROCESSING; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java new file mode 100644 index 0000000000..73367e2af5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -0,0 +1,136 @@ +/** + * 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.job; + +import jakarta.annotation.PreDestroy; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; + +@TbCoreComponent +@Component +@Slf4j +public class DefaultJobManager implements JobManager { + + private final JobService jobService; + private final TbCoreQueueFactory queueFactory; + private final Map jobProcessors; + private final Map>> taskProducers; + private final QueueConsumerManager> taskResultConsumer; + private final ExecutorService consumerExecutor; + + public DefaultJobManager(JobService jobService, TbCoreQueueFactory queueFactory, List jobProcessors) { + this.jobService = jobService; + this.queueFactory = queueFactory; + this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); + this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); + this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("task-result-consumer")); + this.taskResultConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition + .name("tasks-results") + .msgPackProcessor(this::processResults) + .pollInterval(125) + .consumerCreator(queueFactory::createTaskResultConsumer) + .consumerExecutor(consumerExecutor) + .build(); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + taskResultConsumer.subscribe(); + taskResultConsumer.launch(); + } + + @Override + public void submitJob(Job job) { + job = jobService.createJob(job.getTenantId(), job); + log.info("Submitting job: {}", job); + jobProcessors.get(job.getType()).process(job, this::submitTask); + } + + private void submitTask(Task task) { + log.info("Submitting task: {}", task); + TaskProto taskProto = TaskProto.newBuilder() + .setValue(JacksonUtil.toString(task)) + .build(); + + TbQueueProducer> producer = taskProducers.get(task.getJobType()); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(task.getTenantId().getId(), taskProto); // one job at a time for a given tenant + producer.send(TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(), msg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.trace("Submitted task: {}", task); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to submit task: {}", task, t); + } + }); + } + + @SneakyThrows + private void processResults(List> msgs, TbQueueConsumer> consumer) { + Map> results = msgs.stream() + .map(msg -> JacksonUtil.fromString(msg.getValue().getValue(), TaskResult.class)) + .collect(Collectors.groupingBy(TaskResult::getJobId)); + results.forEach((jobId, taskResults) -> { + try { + log.info("[{}] Processing task results: {}", jobId, taskResults); + jobService.reportTaskResults(jobId, taskResults); + } catch (Exception e) { + log.warn("Failed to report task results for job {}: {}", jobId, taskResults, e); + } + }); + consumer.commit(); + + Thread.sleep(5000); + } + + @PreDestroy + private void destroy() { + taskResultConsumer.stop(); + consumerExecutor.shutdownNow(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java new file mode 100644 index 0000000000..5db52ef448 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java @@ -0,0 +1,24 @@ +/** + * 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.job; + +import org.thingsboard.server.common.data.job.Job; + +public interface JobManager { + + void submitJob(Job job); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java new file mode 100644 index 0000000000..cf15209616 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -0,0 +1,30 @@ +/** + * 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.job; + +import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobType; + +import java.util.function.Consumer; + +public abstract class JobProcessor { + + public abstract void process(Job job, Consumer taskConsumer); + + public abstract JobType getType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java new file mode 100644 index 0000000000..36899516f9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java @@ -0,0 +1,57 @@ +/** + * 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.job.task; + +import com.google.common.util.concurrent.SettableFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldReprocessingService; +import org.thingsboard.server.common.data.job.CfReprocessingTask; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.queue.task.TaskProcessor; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class CfReprocessingTaskProcessor extends TaskProcessor { + + private final CalculatedFieldReprocessingService cfReprocessingService; + + @Override + protected void process(CfReprocessingTask task) throws Exception { + SettableFuture future = SettableFuture.create(); + cfReprocessingService.reprocess(task, new TbCallback() { + @Override + public void onSuccess() { + future.set(null); + } + + @Override + public void onFailure(Throwable t) { + future.setException(t); + } + }); + future.get(1, TimeUnit.MINUTES); + } + + @Override + public JobType getJobType() { + return JobType.CF_REPROCESSING; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 418263076b..6670140b30 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1667,6 +1667,8 @@ queue: edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for EDQS state topic (infinite retention, compaction) edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1;cleanup.policy:compact}" + # Kafka properties for tasks topics + tasks: "${TB_QUEUE_KAFKA_TASKS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 13c93da411..a0efcf52c1 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,7 @@ - + diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java new file mode 100644 index 0000000000..17e7645605 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.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.dao.task; + +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.job.Job; + +import java.util.List; + +public interface JobService { + + Job createJob(TenantId tenantId, Job job); + + void reportTaskResults(JobId jobId, List results); + + PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 93e754eb2c..31cc983168 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -63,7 +63,8 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40); + CALCULATED_FIELD_LINK(40), + JOB(41); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index f5dd4b12a0..dcf59a4ea4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -117,6 +117,8 @@ public class EntityIdFactory { return new CalculatedFieldId(uuid); case CALCULATED_FIELD_LINK: return new CalculatedFieldLinkId(uuid); + case JOB: + return new JobId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java new file mode 100644 index 0000000000..e6688f0eb0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -0,0 +1,38 @@ +/** + * 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.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class JobId extends UUIDBased implements EntityId { + + @JsonCreator + public JobId(@JsonProperty("id") UUID id) { + super(id); + } + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TASK", allowableValues = "TASK") + @Override + public EntityType getEntityType() { + return EntityType.JOB; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java new file mode 100644 index 0000000000..4f5a33dacd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java @@ -0,0 +1,39 @@ +/** + * 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.common.data.job; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.cf.CalculatedField; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CfReprocessingJobConfiguration implements JobConfiguration { + + private CalculatedField calculatedField; + private long startTs; + private long endTs; + + @Override + public JobType getType() { + return JobType.CF_REPROCESSING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java new file mode 100644 index 0000000000..2d756f6d53 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java @@ -0,0 +1,25 @@ +/** + * 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.common.data.job; + +public class CfReprocessingJobResult extends JobResult { + + @Override + public JobType getJobType() { + return JobType.CF_REPROCESSING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java new file mode 100644 index 0000000000..0e15b9473f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java @@ -0,0 +1,53 @@ +/** + * 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.common.data.job; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CfReprocessingTask extends Task { + + private CalculatedField calculatedField; + private EntityId entityId; + private long startTs; + private long endTs; + + @Builder + public CfReprocessingTask(TenantId tenantId, JobId jobId, String key, CalculatedField calculatedField, EntityId entityId, long startTs, long endTs) { + super(tenantId, jobId, key); + this.calculatedField = calculatedField; + this.entityId = entityId; + this.startTs = startTs; + this.endTs = endTs; + } + + @Override + public JobType getJobType() { + return JobType.CF_REPROCESSING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java new file mode 100644 index 0000000000..e0161cf98e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -0,0 +1,56 @@ +/** + * 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.common.data.job; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Job extends BaseData implements HasTenantId { + + private TenantId tenantId; + private JobType type; + private String key; + private JobStatus status; + private JobConfiguration configuration; + private JobResult result; + + @Builder + public Job(TenantId tenantId, JobType type, String key, JobConfiguration configuration) { + this.tenantId = tenantId; + this.type = type; + this.key = key; + this.configuration = configuration; + this.status = JobStatus.PENDING; + this.result = switch (type) { + case CF_REPROCESSING -> new CfReprocessingJobResult(); + }; + } + + @SuppressWarnings("unchecked") + public C getConfiguration() { + return (C) configuration; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java new file mode 100644 index 0000000000..8899206c49 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -0,0 +1,32 @@ +/** + * 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.common.data.job; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "CF_REPROCESSING", value = CfReprocessingJobConfiguration.class), +}) +public interface JobConfiguration { + + JobType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java new file mode 100644 index 0000000000..406eb0f65e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -0,0 +1,41 @@ +/** + * 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.common.data.job; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "CF_REPROCESSING", value = CfReprocessingJobResult.class), +}) +@Data +public abstract class JobResult { + + private int successfulCount; + private int failedCount; + private int totalCount; + private Map failures = new HashMap<>(); + + public abstract JobType getJobType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java new file mode 100644 index 0000000000..026e19c5b2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java @@ -0,0 +1,24 @@ +/** + * 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.common.data.job; + +public enum JobStatus { + PENDING, + RUNNING, + COMPLETED, + FAILED, + CANCELLED +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java new file mode 100644 index 0000000000..60cac8173f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java @@ -0,0 +1,26 @@ +/** + * 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.common.data.job; + +public enum JobType { + + CF_REPROCESSING; + + public String getTasksTopic() { + return "tasks." + name().toLowerCase(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java new file mode 100644 index 0000000000..afeaeba393 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java @@ -0,0 +1,50 @@ +/** + * 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.common.data.job; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "CF_REPROCESSING", value = CfReprocessingTask.class), +}) +public abstract class Task { + + private TenantId tenantId; + private JobId jobId; + private String key; + + public Task(TenantId tenantId, JobId jobId, String key) { + this.tenantId = tenantId; + this.jobId = jobId; + this.key = key; + } + + public Task() { + } + + private int attempt = 0; + + public abstract JobType getJobType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java new file mode 100644 index 0000000000..bfbef46180 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java @@ -0,0 +1,45 @@ +/** + * 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.common.data.job; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TaskResult { + + private TenantId tenantId; + private JobId jobId; + private boolean success; + private TaskFailure failure; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class TaskFailure { + private String error; + private Task task; + } + +} diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 938a1692ae..de03b11b6a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1846,3 +1846,11 @@ message EdqsRequestMsg { message EdqsResponseMsg { string value = 1; } + +message TaskProto { + string value = 1; // fixme: TMP, make more efficient +} + +message TaskResultProto { + string value = 1; // fixme: TMP, make more efficient +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index aebda5a5bc..5d5834d20a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -62,6 +62,8 @@ public class TbKafkaTopicConfigs { private String edqsRequestsProperties; @Value("${queue.kafka.topic-properties.edqs-state:}") private String edqsStateProperties; + @Value("${queue.kafka.topic-properties.tasks:}") + private String tasksProperties; @Getter private Map coreConfigs; @@ -99,6 +101,8 @@ public class TbKafkaTopicConfigs { private Map edqsRequestsConfigs; @Getter private Map edqsStateConfigs; + @Getter + private Map tasksConfigs; @PostConstruct private void init() { @@ -122,6 +126,7 @@ public class TbKafkaTopicConfigs { edqsEventsConfigs = PropertyUtils.getProps(edqsEventsProperties); edqsRequestsConfigs = PropertyUtils.getProps(edqsRequestsProperties); edqsStateConfigs = PropertyUtils.getProps(edqsStateProperties); + tasksConfigs = PropertyUtils.getProps(tasksProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 085d04f28c..c8866e52b4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -258,9 +259,28 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE .build(); } + @Override + public TbQueueProducer> createTaskProducer(JobType jobType) { + return new InMemoryTbQueueProducer<>(storage, jobType.getTasksTopic()); + } + + @Override + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return new InMemoryTbQueueConsumer<>(storage, jobType.getTasksTopic()); + } + + @Override + public TbQueueProducer> createTaskResultProducer() { + return new InMemoryTbQueueProducer<>(storage, "tasks.results"); + } + + @Override + public TbQueueConsumer> createTaskResultConsumer() { + return new InMemoryTbQueueConsumer<>(storage, "tasks.results"); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 2269004f90..809099aa8b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -23,12 +23,15 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -110,6 +113,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin cfStateAdmin; private final TbQueueAdmin edqsEventsAdmin; private final TbKafkaAdmin edqsRequestsAdmin; + private final TbQueueAdmin tasksAdmin; private final AtomicLong consumerCount = new AtomicLong(); private final AtomicLong edgeConsumerCount = new AtomicLong(); @@ -158,6 +162,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.cfStateAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldStateConfigs()); this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsRequestsConfigs()); + this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); } @Override @@ -641,6 +646,52 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .build(); } + @Override + public TbQueueProducer> createTaskProducer(JobType jobType) { + return TbKafkaProducerTemplate.>builder() + .clientId(jobType.name().toLowerCase() + "-task-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(jobType.getTasksTopic())) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(jobType.getTasksTopic())) + .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createTaskResultProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("task-result-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("tasks.results")) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createTaskResultConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName("tasks.results")) + .clientId("task-result-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("task-result-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskResultProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index ea7c56f0aa..71a9669ba4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -22,9 +22,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -105,6 +108,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin cfAdmin; private final TbQueueAdmin edqsEventsAdmin; private final TbKafkaAdmin edqsRequestsAdmin; + private final TbQueueAdmin tasksAdmin; private final AtomicLong consumerCount = new AtomicLong(); private final AtomicLong edgeConsumerCount = new AtomicLong(); @@ -153,6 +157,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.cfAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldConfigs()); this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsRequestsConfigs()); + this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); } @Override @@ -520,6 +525,29 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .build(); } + @Override + public TbQueueProducer> createTaskProducer(JobType jobType) { + return TbKafkaProducerTemplate.>builder() + .clientId(jobType.name().toLowerCase() + "-task-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(jobType.getTasksTopic())) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createTaskResultConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName("tasks.results")) + .clientId("task-result-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("task-result-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TaskResultProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index b4884ae72c..d0ac20ae06 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -22,12 +22,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -96,6 +99,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin cfAdmin; private final TbQueueAdmin cfStateAdmin; private final TbQueueAdmin edqsEventsAdmin; + private final TbQueueAdmin tasksAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -133,6 +137,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.cfAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldConfigs()); this.cfStateAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldStateConfigs()); this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); + this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); } @Override @@ -414,6 +419,29 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { throw new UnsupportedOperationException(); } + @Override + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(jobType.getTasksTopic())) + .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createTaskResultProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("task-result-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("tasks.results")) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -438,4 +466,5 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { cfAdmin.destroy(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java new file mode 100644 index 0000000000..10b84f0f65 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java @@ -0,0 +1,31 @@ +/** + * 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.queue.provider; + +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; + +public interface TaskProcessorQueueFactory { + + TbQueueConsumer> createTaskConsumer(JobType jobType); + + TbQueueProducer> createTaskResultProducer(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 037d1f2087..409348a12a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -17,7 +17,10 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -165,4 +168,8 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous TbQueueProducer> createToCalculatedFieldNotificationMsgProducer(); + TbQueueProducer> createTaskProducer(JobType jobType); + + TbQueueConsumer> createTaskResultConsumer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 18bb6db14a..83c467c992 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -41,7 +41,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { +public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory, TaskProcessorQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java new file mode 100644 index 0000000000..7123eaf1e0 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -0,0 +1,139 @@ +/** + * 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.queue.task; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Slf4j +public abstract class TaskProcessor { + + @Autowired + private TaskProcessorQueueFactory queueFactory; + + private QueueConsumerManager> taskConsumer; + private TbQueueProducer> taskResultProducer; + private ExecutorService consumerExecutor; + + @PostConstruct + public void init() { + consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-consumer")); + taskConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition + .name(getJobType().name() + "-tasks") + .msgPackProcessor(this::processMsgs) + .pollInterval(125) + .consumerCreator(() -> queueFactory.createTaskConsumer(getJobType())) + .consumerExecutor(consumerExecutor) + .build(); + taskResultProducer = queueFactory.createTaskResultProducer(); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + taskConsumer.subscribe(); + taskConsumer.launch(); + } + + @PreDestroy + public void destroy() { + taskConsumer.stop(); + consumerExecutor.shutdownNow(); + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer) { + for (TbProtoQueueMsg msg : msgs) { + TaskProto taskProto = msg.getValue(); + Task task = JacksonUtil.fromString(taskProto.getValue(), Task.class); + processTask((T) task); + } + consumer.commit(); + } + + private void processTask(T task) { + task.setAttempt(task.getAttempt() + 1); + log.info("Processing task: {}", task); + try { + process(task); + reportSuccess(task); + } catch (Exception e) { + log.error("Failed to process task (attempt {}): {}", task.getAttempt(), task, e); + if (task.getAttempt() < 3) { + processTask(task); + } else { + reportFailure(task, e); + } + } + } + + private void reportSuccess(Task task) { + TaskResult result = TaskResult.builder() + .tenantId(task.getTenantId()) + .jobId(task.getJobId()) + .success(true) + .build(); + reportResult(result); + } + + private void reportFailure(Task task, Throwable error) { + TaskResult result = TaskResult.builder() + .tenantId(task.getTenantId()) + .jobId(task.getJobId()) + .failure(TaskFailure.builder() + .error(error.getMessage()) + .task(task) + .build()) + .build(); + reportResult(result); + } + + private void reportResult(TaskResult result) { + log.info("Reporting result: {}", result); + TaskResultProto resultProto = TaskResultProto.newBuilder() + .setValue(JacksonUtil.toString(result)) + .build(); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(result.getJobId().getId(), resultProto); + taskResultProducer.send(TopicPartitionInfo.builder() + .topic(taskResultProducer.getDefaultTopic()) + .build(), msg, TbQueueCallback.EMPTY); + } + + protected abstract void process(T task) throws Exception; + + public abstract JobType getJobType(); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 148908d063..1d504b0ab2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -739,6 +739,16 @@ public class ModelConstants { public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; + /** + * Tasks constants. + */ + public static final String JOB_TABLE_NAME = "job"; + public static final String JOB_TYPE_PROPERTY = "type"; + public static final String JOB_KEY_PROPERTY = "key"; + public static final String JOB_STATUS_PROPERTY = "status"; + public static final String JOB_CONFIGURATION_PROPERTY = "configuration"; + public static final String JOB_RESULT_PROPERTY = "result"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java new file mode 100644 index 0000000000..6e8b3e7958 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java @@ -0,0 +1,96 @@ +/** + * 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.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLJsonPGObjectJsonbType; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobConfiguration; +import org.thingsboard.server.common.data.job.JobResult; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Entity +@Table(name = ModelConstants.JOB_TABLE_NAME) +public class JobEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.TENANT_ID_PROPERTY, nullable = false) + private UUID tenantId; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.JOB_TYPE_PROPERTY, nullable = false) + private JobType type; + + @Column(name = ModelConstants.JOB_KEY_PROPERTY, nullable = false) + private String key; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.JOB_STATUS_PROPERTY, nullable = false) + private JobStatus status; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.JOB_CONFIGURATION_PROPERTY, nullable = false) + private JsonNode configuration; + + @Convert(converter = JsonConverter.class) + @JdbcType(PostgreSQLJsonPGObjectJsonbType.class) + @Column(name = ModelConstants.JOB_RESULT_PROPERTY) + private JsonNode result; + + public JobEntity(Job job) { + super(job); + this.tenantId = getTenantUuid(job.getTenantId()); + this.type = job.getType(); + this.key = job.getKey(); + this.status = job.getStatus(); + this.configuration = toJson(job.getConfiguration()); + this.result = toJson(job.getResult()); + } + + @Override + public Job toData() { + Job job = new Job(); + job.setId(new JobId(id)); + job.setCreatedTime(createdTime); + job.setTenantId(getTenantId(tenantId)); + job.setType(type); + job.setKey(key); + job.setStatus(status); + job.setConfiguration(fromJson(configuration, JobConfiguration.class)); + job.setResult(fromJson(result, JobResult.class)); + return job; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java new file mode 100644 index 0000000000..273d8ef78a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java @@ -0,0 +1,69 @@ +/** + * 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.dao.sql.task; + +import jakarta.transaction.Transactional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.thingsboard.server.dao.model.sql.JobEntity; + +import java.util.UUID; + +@Repository +public interface JobRepository extends JpaRepository { + + Page findByTenantId(UUID tenantId, Pageable pageable); + + @Modifying + @Transactional + @Query(value = """ + UPDATE job + SET result = jsonb_set( + result, + '{successfulCount}', + to_jsonb((result->>'successfulCount')::int + :count) + ) + WHERE id = :jobId + RETURNING ((result->>'successfulCount')::int + :count) + + (result->>'failedCount')::int = (result->>'totalCount')::int + """, nativeQuery = true) + boolean reportTaskSuccess(@Param("jobId") UUID jobId, @Param("count") int count); + + @Modifying + @Transactional + @Query(value = """ + UPDATE job + SET result = jsonb_set( + jsonb_set( + result, + '{failedCount}', + to_jsonb((result->>'failedCount')::int + 1) + ), + ARRAY['failures', :taskKey], + to_jsonb(:error) + ) + WHERE id = :jobId + RETURNING ((result->>'failedCount')::int + 1) + (result->>'successfulCount')::int + = (result->>'totalCount')::int + """, nativeQuery = true) + boolean reportTaskFailure(@Param("jobId") UUID jobId, @Param("taskKey") String taskKey, @Param("error") String error); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java new file mode 100644 index 0000000000..d6286e2d77 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java @@ -0,0 +1,72 @@ +/** + * 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.dao.sql.task; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.JobEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.task.JobDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; + +@Component +@SqlDao +@RequiredArgsConstructor +public class JpaJobDao extends JpaAbstractDao implements JobDao { + + private final JobRepository jobRepository; + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(jobRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public boolean reportTaskSuccess(JobId jobId, int tasksCount) { + return jobRepository.reportTaskSuccess(jobId.getId(), tasksCount); + } + + @Override + public boolean reportTaskFailure(JobId jobId, String taskKey, String error) { + return jobRepository.reportTaskFailure(jobId.getId(), taskKey, error); + } + + @Override + public EntityType getEntityType() { + return EntityType.JOB; + } + + @Override + protected Class getEntityClass() { + return JobEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return jobRepository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java new file mode 100644 index 0000000000..aa9e48600c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java @@ -0,0 +1,86 @@ +/** + * 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.dao.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobResult; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultJobService implements JobService { + + private final JobDao jobDao; + + @Override + public Job createJob(TenantId tenantId, Job job) { + return jobDao.save(tenantId, job); + } + + @Override + public void reportTaskResults(JobId jobId, List results) { + Job job = jobDao.findById(TenantId.SYS_TENANT_ID, jobId.getId()); + switch (job.getStatus()) { + case PENDING -> { + job.setStatus(JobStatus.RUNNING); + } + case CANCELLED, COMPLETED, FAILED -> { + // got some stale stats + return; + } + } + + JobResult jobResult = job.getResult(); + for (TaskResult taskResult : results) { + if (taskResult.isSuccess()) { + jobResult.setSuccessfulCount(jobResult.getSuccessfulCount() + 1); + } else { + TaskFailure failure = taskResult.getFailure(); + String key = failure.getTask().getKey(); + jobResult.setFailedCount(jobResult.getFailedCount() + 1); + jobResult.getFailures().put(key, failure.getError()); + } + } + + if (jobResult.getSuccessfulCount() + jobResult.getFailedCount() >= jobResult.getTotalCount()) { + if (jobResult.getFailures().isEmpty()) { + job.setStatus(JobStatus.COMPLETED); + } else { + job.setStatus(JobStatus.FAILED); + } + } + log.info("Saving job {}", job); + jobDao.save(TenantId.SYS_TENANT_ID, job); + } + + @Override + public PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink) { + return jobDao.findByTenantId(tenantId, pageLink); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java new file mode 100644 index 0000000000..6a5b002aea --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java @@ -0,0 +1,33 @@ +/** + * 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.dao.task; + +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +public interface JobDao extends Dao { + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + boolean reportTaskSuccess(JobId jobId, int tasksCount); + + boolean reportTaskFailure(JobId jobId, String taskKey, String error); + +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b425550e7e..7fa31da5fb 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -948,3 +948,14 @@ CREATE TABLE IF NOT EXISTS cf_debug_event ( e_result varchar, e_error varchar ) PARTITION BY RANGE (ts); + +CREATE TABLE IF NOT EXISTS job ( + id uuid NOT NULL CONSTRAINT job_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid NOT NULL, + type varchar NOT NULL, + key varchar NOT NULL, + status varchar NOT NULL, + configuration varchar(1000) NOT NULL, + result jsonb +); From 290fba4819a807d0275b5fca21b84b520702b8f3 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 24 Apr 2025 15:32:22 +0300 Subject: [PATCH 02/44] Job stats, REST API, tests --- .../server/controller/JobController.java | 73 ++++++++++ .../job/CfReprocessingJobProcessor.java | 37 ++++-- .../server/service/job/DefaultJobManager.java | 64 ++++++--- .../server/service/job/DummyJobProcessor.java | 64 +++++++++ .../server/service/job/JobManager.java | 2 +- .../server/service/job/JobProcessor.java | 2 +- .../service/job/task/DummyTaskProcessor.java | 44 ++++++ .../src/main/resources/thingsboard.yml | 4 + .../server/service/job/JobManagerTest.java | 125 ++++++++++++++++++ .../server/dao/task/JobService.java | 10 +- .../job/CfReprocessingJobConfiguration.java | 2 + .../common/data/job/CfReprocessingTask.java | 16 +-- .../data/job/DummyJobConfiguration.java | 42 ++++++ .../common/data/job/DummyJobResult.java | 25 ++++ .../server/common/data/job/DummyTask.java | 40 ++++++ .../server/common/data/job/Job.java | 12 +- .../common/data/job/JobConfiguration.java | 1 + .../server/common/data/job/JobResult.java | 8 +- .../server/common/data/job/JobStats.java | 29 ++++ .../server/common/data/job/JobType.java | 3 +- .../server/common/data/job/Task.java | 15 ++- .../server/common/data/job/TaskResult.java | 4 - common/proto/src/main/proto/queue.proto | 7 + .../InMemoryMonolithQueueFactory.java | 9 +- .../provider/KafkaMonolithQueueFactory.java | 22 +-- .../provider/KafkaTbCoreQueueFactory.java | 37 +++++- .../KafkaTbRuleEngineQueueFactory.java | 10 +- .../provider/TaskProcessorQueueFactory.java | 4 +- .../queue/provider/TbCoreQueueFactory.java | 6 +- .../provider/TbCoreQueueProducerProvider.java | 8 ++ .../provider/TbQueueProducerProvider.java | 3 + .../TbRuleEngineProducerProvider.java | 8 ++ .../TbTransportQueueProducerProvider.java | 6 + .../TbVersionControlProducerProvider.java | 5 + .../server/queue/task/JobStatsService.java | 63 +++++++++ .../server/queue/task/TaskProcessor.java | 31 +---- .../server/dao/model/ModelConstants.java | 1 + .../server/dao/model/sql/JobEntity.java | 5 + .../server/dao/sql/task/JobRepository.java | 21 ++- .../server/dao/sql/task/JpaJobDao.java | 16 ++- .../server/dao/task/DefaultJobService.java | 40 +++++- .../thingsboard/server/dao/task/JobDao.java | 6 + .../main/resources/sql/schema-entities.sql | 1 + 43 files changed, 795 insertions(+), 136 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/JobController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java create mode 100644 application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java new file mode 100644 index 0000000000..f7bc5255d3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -0,0 +1,73 @@ +/** + * 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.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class JobController extends BaseController { + + private final JobService jobService; + + @GetMapping("/job/{id}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public Job getJobById(@PathVariable UUID id) throws ThingsboardException { + return jobService.findJobById(getTenantId(), new JobId(id)); + } + + @GetMapping("/jobs") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getJobs(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on job's description") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return jobService.findJobsByTenantId(getTenantId(), pageLink); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java index 3b63d3736f..b5f6c4665f 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java @@ -17,9 +17,11 @@ package org.thingsboard.server.service.job; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.CfReprocessingJobConfiguration; import org.thingsboard.server.common.data.job.CfReprocessingTask; @@ -39,28 +41,34 @@ public class CfReprocessingJobProcessor extends JobProcessor { private final DeviceService deviceService; private final AssetService assetService; + // fixme: multiple jobs with single type + @Transactional @Override - public void process(Job job, Consumer taskConsumer) { + public int process(Job job, Consumer taskConsumer) { CfReprocessingJobConfiguration configuration = job.getConfiguration(); CalculatedField calculatedField = configuration.getCalculatedField(); - EntityId entityId = calculatedField.getEntityId(); + EntityId cfEntityId = calculatedField.getEntityId(); - if (entityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { - taskConsumer.accept(createTask(job, configuration, entityId)); + int tasksCount = 0; + if (cfEntityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { + taskConsumer.accept(createTask(job, configuration, cfEntityId)); + tasksCount++; } else { - PageDataIterable entities; - if (entityId.getEntityType() == EntityType.DEVICE_PROFILE) { - entities = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(job.getTenantId(), pageLink), 512); - } else if (entityId.getEntityType() == EntityType.ASSET_PROFILE) { - entities = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(job.getTenantId(), pageLink), 512); + PageDataIterable entities; + if (cfEntityId.getEntityType() == EntityType.DEVICE_PROFILE) { + entities = new PageDataIterable<>(pageLink -> deviceService.findDeviceIdsByTenantIdAndDeviceProfileId(job.getTenantId(), (DeviceProfileId) cfEntityId, pageLink), 512); + } else if (cfEntityId.getEntityType() == EntityType.ASSET_PROFILE) { + entities = new PageDataIterable<>(pageLink -> assetService.findAssetIdsByTenantIdAndAssetProfileId(job.getTenantId(), (AssetProfileId) cfEntityId, pageLink), 512); } else { - throw new IllegalArgumentException("Unsupported CF entity type " + entityId.getEntityType()); + throw new IllegalArgumentException("Unsupported CF entity type " + cfEntityId.getEntityType()); + } + for (EntityId entityId : entities) { + taskConsumer.accept(createTask(job, configuration, entityId)); + tasksCount++; } - entities.forEach(device -> { - taskConsumer.accept(createTask(job, configuration, device.getEntityId())); - }); } + return tasksCount; } private Task createTask(Job job, CfReprocessingJobConfiguration configuration, EntityId entityId) { @@ -68,6 +76,7 @@ public class CfReprocessingJobProcessor extends JobProcessor { .tenantId(job.getTenantId()) .jobId(job.getId()) .key(entityId.getEntityType().getNormalName() + " " + entityId.getId()) + .retries(2) // 3 attempts in total .calculatedField(configuration.getCalculatedField()) .entityId(entityId) .startTs(configuration.getStartTs()) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 73367e2af5..64c1615310 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -18,18 +18,20 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; -import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -37,12 +39,15 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Function; @@ -54,23 +59,26 @@ import java.util.stream.Collectors; public class DefaultJobManager implements JobManager { private final JobService jobService; - private final TbCoreQueueFactory queueFactory; + private final JobStatsService jobStatsService; private final Map jobProcessors; private final Map>> taskProducers; - private final QueueConsumerManager> taskResultConsumer; + private final QueueConsumerManager> taskResultConsumer; private final ExecutorService consumerExecutor; - public DefaultJobManager(JobService jobService, TbCoreQueueFactory queueFactory, List jobProcessors) { + @Value("${queue.tasks.stats.processing_interval_ms:5000}") + private int statsProcessingInterval; + + public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, TbCoreQueueFactory queueFactory, List jobProcessors) { this.jobService = jobService; - this.queueFactory = queueFactory; + this.jobStatsService = jobStatsService; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("task-result-consumer")); - this.taskResultConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition - .name("tasks-results") - .msgPackProcessor(this::processResults) + this.taskResultConsumer = QueueConsumerManager.>builder() + .name("job-stats") + .msgPackProcessor(this::processStats) .pollInterval(125) - .consumerCreator(queueFactory::createTaskResultConsumer) + .consumerCreator(queueFactory::createJobStatsConsumer) .consumerExecutor(consumerExecutor) .build(); } @@ -82,10 +90,13 @@ public class DefaultJobManager implements JobManager { } @Override - public void submitJob(Job job) { + public Job submitJob(Job job) { job = jobService.createJob(job.getTenantId(), job); log.info("Submitting job: {}", job); - jobProcessors.get(job.getType()).process(job, this::submitTask); + + int tasksCount = jobProcessors.get(job.getType()).process(job, this::submitTask); + jobStatsService.reportAllTasksSubmitted(job.getId(), tasksCount); + return job; } private void submitTask(Task task) { @@ -110,21 +121,34 @@ public class DefaultJobManager implements JobManager { } @SneakyThrows - private void processResults(List> msgs, TbQueueConsumer> consumer) { - Map> results = msgs.stream() - .map(msg -> JacksonUtil.fromString(msg.getValue().getValue(), TaskResult.class)) - .collect(Collectors.groupingBy(TaskResult::getJobId)); - results.forEach((jobId, taskResults) -> { + private void processStats(List> msgs, TbQueueConsumer> consumer) { + Map stats = new HashMap<>(); + + for (TbProtoQueueMsg msg : msgs) { + JobStatsMsg statsMsg = msg.getValue(); + JobId jobId = new JobId(new UUID(statsMsg.getJobIdMSB(), statsMsg.getJobIdLSB())); + JobStats jobStats = stats.computeIfAbsent(jobId, JobStats::new); + + if (statsMsg.hasTaskResult()) { + TaskResult taskResult = JacksonUtil.fromString(statsMsg.getTaskResult().getValue(), TaskResult.class); + jobStats.getTaskResults().add(taskResult); + } + if (statsMsg.hasTotalTasksCount()) { + jobStats.setTotalTasksCount(statsMsg.getTotalTasksCount()); + } + } + + stats.forEach((jobId, jobStats) -> { try { - log.info("[{}] Processing task results: {}", jobId, taskResults); - jobService.reportTaskResults(jobId, taskResults); + log.info("[{}] Processing job stats: {}", jobId, stats); + jobService.processStats(jobId, jobStats); } catch (Exception e) { - log.warn("Failed to report task results for job {}: {}", jobId, taskResults, e); + log.warn("Failed to process job stats for {}: {}", jobId, jobStats, e); } }); consumer.commit(); - Thread.sleep(5000); + Thread.sleep(statsProcessingInterval); } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java new file mode 100644 index 0000000000..bed8f3f25e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -0,0 +1,64 @@ +/** + * 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.job; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.DummyJobConfiguration; +import org.thingsboard.server.common.data.job.DummyTask; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.Task; + +import java.util.List; +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +public class DummyJobProcessor extends JobProcessor { + + @Override + public int process(Job job, Consumer taskConsumer) { + DummyJobConfiguration configuration = job.getConfiguration(); + for (int number = 1; number <= configuration.getSuccessfulTasksCount(); number++) { + taskConsumer.accept(createTask(job, configuration, number, null)); + } + if (configuration.getErrors() != null) { + for (int number = 1; number <= configuration.getFailedTasksCount(); number++) { + taskConsumer.accept(createTask(job, configuration, number, configuration.getErrors())); + } + } + return configuration.getSuccessfulTasksCount() + configuration.getFailedTasksCount(); + } + + private Task createTask(Job job, DummyJobConfiguration configuration, int number, List errors) { + return DummyTask.builder() + .tenantId(job.getTenantId()) + .jobId(job.getId()) + .key("Task " + number) + .retries(configuration.getRetries()) + .number(number) + .processingTimeMs(configuration.getTaskProcessingTimeMs()) + .errors(errors) + .build(); + } + + @Override + public JobType getType() { + return JobType.DUMMY; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java index 5db52ef448..c78de2a5d9 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java @@ -19,6 +19,6 @@ import org.thingsboard.server.common.data.job.Job; public interface JobManager { - void submitJob(Job job); + Job submitJob(Job job); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index cf15209616..01f7291dd3 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -23,7 +23,7 @@ import java.util.function.Consumer; public abstract class JobProcessor { - public abstract void process(Job job, Consumer taskConsumer); + public abstract int process(Job job, Consumer taskConsumer); public abstract JobType getType(); diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java new file mode 100644 index 0000000000..10aafc197d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -0,0 +1,44 @@ +/** + * 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.job.task; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.DummyTask; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.queue.task.TaskProcessor; + +@Component +@RequiredArgsConstructor +public class DummyTaskProcessor extends TaskProcessor { + + @Override + protected void process(DummyTask task) throws Exception { + if (task.getProcessingTimeMs() > 0) { + Thread.sleep(task.getProcessingTimeMs()); + } + if (task.getErrors() != null && task.getAttempt() <= task.getErrors().size()) { + String error = task.getErrors().get(task.getAttempt() - 1); + throw new RuntimeException(error); + } + } + + @Override + public JobType getJobType() { + return JobType.DUMMY; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 6670140b30..322b037e2d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1883,6 +1883,10 @@ queue: enabled: "${TB_QUEUE_EDGE_STATS_ENABLED:true}" # Statistics printing interval for Edge services print-interval-ms: "${TB_QUEUE_EDGE_STATS_PRINT_INTERVAL_MS:60000}" + tasks: + stats: + # Interval in milliseconds to process job stats + processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:5000}" # Event configuration parameters event: diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java new file mode 100644 index 0000000000..f9ee2e2b13 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -0,0 +1,125 @@ +/** + * 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.job; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.job.DummyJobConfiguration; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@DaoSqlTest +@TestPropertySource(properties = { + "queue.tasks.stats.processing_interval_ms=0" +}) +public class JobManagerTest extends AbstractControllerTest { + + @Autowired + private JobManager jobManager; + + @Before + public void setUp() throws Exception { + loginTenantAdmin(); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testSubmitJob_allTasksSuccessful() { + int tasksCount = 5; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING); + assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); + }); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getFailures()).isEmpty(); + }); + } + + @Test + public void testSubmitJob_someTasksPermanentlyFailed() { + int successfulTasks = 3; + int failedTasks = 2; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .errors(List.of("error1", "error2", "error3")) + .retries(2) + .taskProcessingTimeMs(100) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(successfulTasks); + assertThat(job.getResult().getFailedCount()).isEqualTo(failedTasks); + assertThat(job.getResult().getTotalCount()).isEqualTo(successfulTasks + failedTasks); + assertThat(job.getResult().getFailures().get("Task 1")).isEqualTo("error3"); // last error + assertThat(job.getResult().getFailures().get("Task 2")).isEqualTo("error3"); // last error + }); + } + + + + private Job findJobById(JobId jobId) throws Exception { + return doGet("/api/job/" + jobId, Job.class); + } + + private List findJobs() throws Exception { + return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0)).getData(); + } + +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java index 17e7645605..5581f1eac7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java @@ -17,18 +17,18 @@ package org.thingsboard.server.dao.task; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.job.Job; - -import java.util.List; public interface JobService { Job createJob(TenantId tenantId, Job job); - void reportTaskResults(JobId jobId, List results); + Job findJobById(TenantId tenantId, JobId jobId); + + void processStats(JobId jobId, JobStats jobStats); PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java index 4f5a33dacd..797dd0b639 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField; @Builder public class CfReprocessingJobConfiguration implements JobConfiguration { + @NotNull private CalculatedField calculatedField; private long startTs; private long endTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java index 0e15b9473f..5c380c4dfa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java @@ -15,20 +15,17 @@ */ package org.thingsboard.server.common.data.job; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.JobId; -import org.thingsboard.server.common.data.id.TenantId; @Data -@AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = true) +@SuperBuilder public class CfReprocessingTask extends Task { private CalculatedField calculatedField; @@ -36,15 +33,6 @@ public class CfReprocessingTask extends Task { private long startTs; private long endTs; - @Builder - public CfReprocessingTask(TenantId tenantId, JobId jobId, String key, CalculatedField calculatedField, EntityId entityId, long startTs, long endTs) { - super(tenantId, jobId, key); - this.calculatedField = calculatedField; - this.entityId = entityId; - this.startTs = startTs; - this.endTs = endTs; - } - @Override public JobType getJobType() { return JobType.CF_REPROCESSING; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java new file mode 100644 index 0000000000..7fe621c058 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -0,0 +1,42 @@ +/** + * 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.common.data.job; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DummyJobConfiguration implements JobConfiguration { + + private long taskProcessingTimeMs; + private int successfulTasksCount; + private int failedTasksCount; + private List errors; + private int retries; + + @Override + public JobType getType() { + return JobType.DUMMY; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java new file mode 100644 index 0000000000..031a733d51 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java @@ -0,0 +1,25 @@ +/** + * 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.common.data.job; + +public class DummyJobResult extends JobResult { + + @Override + public JobType getJobType() { + return JobType.DUMMY; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java new file mode 100644 index 0000000000..dee97fc3c9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java @@ -0,0 +1,40 @@ +/** + * 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.common.data.job; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +public class DummyTask extends Task { + + private int number; + private long processingTimeMs; + private List errors; // errors for each attempt + + @Override + public JobType getJobType() { + return JobType.DUMMY; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index e0161cf98e..5a51a49239 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.job; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; @@ -29,22 +31,30 @@ import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) public class Job extends BaseData implements HasTenantId { + @NotNull private TenantId tenantId; + @NotNull private JobType type; + @NotBlank private String key; + @NotBlank + private String description; private JobStatus status; + @NotNull private JobConfiguration configuration; private JobResult result; @Builder - public Job(TenantId tenantId, JobType type, String key, JobConfiguration configuration) { + public Job(TenantId tenantId, JobType type, String key, String description, JobConfiguration configuration) { this.tenantId = tenantId; this.type = type; this.key = key; + this.description = description; this.configuration = configuration; this.status = JobStatus.PENDING; this.result = switch (type) { case CF_REPROCESSING -> new CfReprocessingJobResult(); + case DUMMY -> new DummyJobResult(); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 8899206c49..eccdfae6bb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "CF_REPROCESSING", value = CfReprocessingJobConfiguration.class), + @Type(name = "DUMMY", value = DummyJobConfiguration.class), }) public interface JobConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 406eb0f65e..07b6c4eadd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.job; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.HashMap; import java.util.Map; @@ -26,14 +28,16 @@ import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @JsonSubTypes.Type(name = "CF_REPROCESSING", value = CfReprocessingJobResult.class), + @Type(name = "CF_REPROCESSING", value = CfReprocessingJobResult.class), + @Type(name = "DUMMY", value = DummyJobResult.class) }) @Data +@NoArgsConstructor public abstract class JobResult { private int successfulCount; private int failedCount; - private int totalCount; + private Integer totalCount = null; // set when all tasks are submitted private Map failures = new HashMap<>(); public abstract JobType getJobType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java new file mode 100644 index 0000000000..6491d0998a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java @@ -0,0 +1,29 @@ +/** + * 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.common.data.job; + +import lombok.Data; +import org.thingsboard.server.common.data.id.JobId; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class JobStats { + private final JobId jobId; + private final List taskResults = new ArrayList<>(); + private Integer totalTasksCount; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java index 60cac8173f..7c0d9972e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java @@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.job; public enum JobType { - CF_REPROCESSING; + CF_REPROCESSING, + DUMMY; public String getTasksTopic() { return "tasks." + name().toLowerCase(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java index afeaeba393..5ef735f5d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java @@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.job; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -26,19 +29,17 @@ import org.thingsboard.server.common.data.id.TenantId; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @JsonSubTypes.Type(name = "CF_REPROCESSING", value = CfReprocessingTask.class), + @Type(name = "CF_REPROCESSING", value = CfReprocessingTask.class), + @Type(name = "DUMMY", value = DummyTask.class) }) +@SuperBuilder +@AllArgsConstructor public abstract class Task { private TenantId tenantId; private JobId jobId; private String key; - - public Task(TenantId tenantId, JobId jobId, String key) { - this.tenantId = tenantId; - this.jobId = jobId; - this.key = key; - } + private int retries; public Task() { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java index bfbef46180..ae5e6bbba9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java @@ -19,8 +19,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.id.JobId; -import org.thingsboard.server.common.data.id.TenantId; @Data @AllArgsConstructor @@ -28,8 +26,6 @@ import org.thingsboard.server.common.data.id.TenantId; @Builder public class TaskResult { - private TenantId tenantId; - private JobId jobId; private boolean success; private TaskFailure failure; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index de03b11b6a..032301206f 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1851,6 +1851,13 @@ message TaskProto { string value = 1; // fixme: TMP, make more efficient } +message JobStatsMsg { + int64 jobIdMSB = 1; + int64 jobIdLSB = 2; + optional TaskResultProto taskResult = 3; + optional int32 totalTasksCount = 4; +} + message TaskResultProto { string value = 1; // fixme: TMP, make more efficient } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index c8866e52b4..9160818278 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -28,6 +28,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; @@ -270,13 +271,13 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createTaskResultProducer() { - return new InMemoryTbQueueProducer<>(storage, "tasks.results"); + public TbQueueProducer> createJobStatsProducer() { + return new InMemoryTbQueueProducer<>(storage, "jobs.stats"); } @Override - public TbQueueConsumer> createTaskResultConsumer() { - return new InMemoryTbQueueConsumer<>(storage, "tasks.results"); + public TbQueueConsumer> createJobStatsConsumer() { + return new InMemoryTbQueueConsumer<>(storage, "jobs.stats"); } @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 809099aa8b..e4b51eaa1f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -30,8 +30,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; -import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -670,23 +670,23 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createTaskResultProducer() { - return TbKafkaProducerTemplate.>builder() - .clientId("task-result-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("tasks.results")) + public TbQueueProducer> createJobStatsProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("jobs.stats")) .settings(kafkaSettings) .admin(tasksAdmin) .build(); } @Override - public TbQueueConsumer> createTaskResultConsumer() { - return TbKafkaConsumerTemplate.>builder() + public TbQueueConsumer> createJobStatsConsumer() { + return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName("tasks.results")) - .clientId("task-result-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName("task-result-consumer-group")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskResultProto.parseFrom(msg.getData()), msg.getHeaders())) + .topic(topicService.buildTopicName("jobs.stats")) + .clientId("job-stats-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("job-stats-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), JobStatsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(tasksAdmin) .statsService(consumerStatsService) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 71a9669ba4..85d1e8be14 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -25,8 +25,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; @@ -536,13 +536,36 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createTaskResultConsumer() { - return TbKafkaConsumerTemplate.>builder() + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName("tasks.results")) - .clientId("task-result-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName("task-result-consumer-group")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TaskResultProto.parseFrom(msg.getData()), msg.getHeaders())) + .topic(topicService.buildTopicName(jobType.getTasksTopic())) + .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createJobStatsProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("jobs.stats")) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createJobStatsConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName("jobs.stats")) + .clientId("job-stats-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("job-stats-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), JobStatsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(tasksAdmin) .statsService(consumerStatsService) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index d0ac20ae06..b2af3671c6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -27,9 +27,9 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; @@ -433,10 +433,10 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createTaskResultProducer() { - return TbKafkaProducerTemplate.>builder() - .clientId("task-result-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("tasks.results")) + public TbQueueProducer> createJobStatsProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("jobs.stats")) .settings(kafkaSettings) .admin(tasksAdmin) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java index 10b84f0f65..571b14639c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java @@ -16,8 +16,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; -import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -26,6 +26,6 @@ public interface TaskProcessorQueueFactory { TbQueueConsumer> createTaskConsumer(JobType jobType); - TbQueueProducer> createTaskResultProducer(); + TbQueueProducer> createJobStatsProducer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 409348a12a..823ebea298 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; -import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -47,7 +47,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { +public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory, TaskProcessorQueueFactory { /** * Used to push messages to instances of TB Transport Service @@ -170,6 +170,6 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous TbQueueProducer> createTaskProducer(JobType jobType); - TbQueueConsumer> createTaskResultConsumer(); + TbQueueConsumer> createJobStatsConsumer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 98a3d78304..9900474a10 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -53,6 +54,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toHousekeeper; private TbQueueProducer> toCalculatedFields; private TbQueueProducer> toCalculatedFieldNotifications; + private TbQueueProducer> jobStatsProducer; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -73,6 +75,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toEdgeEvents = tbQueueProvider.createEdgeEventMsgProducer(); this.toCalculatedFields = tbQueueProvider.createToCalculatedFieldMsgProducer(); this.toCalculatedFieldNotifications = tbQueueProvider.createToCalculatedFieldNotificationMsgProducer(); + this.jobStatsProducer = tbQueueProvider.createJobStatsProducer(); } @Override @@ -140,4 +143,9 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toCalculatedFieldNotifications; } + @Override + public TbQueueProducer> getJobStatsProducer() { + return jobStatsProducer; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index 865637b2ff..428e673fa8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.queue.provider; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -97,4 +98,6 @@ public interface TbQueueProducerProvider { TbQueueProducer> getCalculatedFieldsNotificationsMsgProducer(); + TbQueueProducer> getJobStatsProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 8e1952fc14..9e77a2d4e7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -51,6 +52,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toEdgeEvents; private TbQueueProducer> toCalculatedFields; private TbQueueProducer> toCalculatedFieldNotifications; + private TbQueueProducer> jobStatsProducer; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -70,6 +72,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toEdgeEvents = tbQueueProvider.createEdgeEventMsgProducer(); this.toCalculatedFields = tbQueueProvider.createToCalculatedFieldMsgProducer(); this.toCalculatedFieldNotifications = tbQueueProvider.createToCalculatedFieldNotificationMsgProducer(); + this.jobStatsProducer = tbQueueProvider.createJobStatsProducer(); } @Override @@ -137,4 +140,9 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { return toCalculatedFieldNotifications; } + @Override + public TbQueueProducer> getJobStatsProducer() { + return jobStatsProducer; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index a7a34992cd..4472c6157e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -121,4 +121,10 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider public TbQueueProducer> getCalculatedFieldsNotificationsMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } + + @Override + public TbQueueProducer> getJobStatsProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 85c400d094..0370f8a4af 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -118,4 +118,9 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } + @Override + public TbQueueProducer> getJobStatsProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java new file mode 100644 index 0000000000..6c36c573ca --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -0,0 +1,63 @@ +/** + * 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.queue.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; + +@Lazy +@Service +@Slf4j +@RequiredArgsConstructor +public class JobStatsService { + + private final TbQueueProducerProvider producerProvider; + + public void reportTaskResult(JobId jobId, TaskResult result) { + report(jobId, JobStatsMsg.newBuilder() + .setTaskResult(TaskResultProto.newBuilder() + .setValue(JacksonUtil.toString(result)) + .build())); + } + + public void reportAllTasksSubmitted(JobId jobId, int tasksCount) { + report(jobId, JobStatsMsg.newBuilder() + .setTotalTasksCount(tasksCount)); + } + + private void report(JobId jobId, JobStatsMsg.Builder statsMsg) { + log.info("[{}] Reporting: {}", jobId, statsMsg); + statsMsg.setJobIdMSB(jobId.getId().getMostSignificantBits()) + .setJobIdLSB(jobId.getId().getLeastSignificantBits()); + + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(jobId.getId(), statsMsg.build()); + TbQueueProducer> producer = producerProvider.getJobStatsProducer(); + producer.send(TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(), msg, TbQueueCallback.EMPTY); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 7123eaf1e0..52fd382c3d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -25,12 +25,8 @@ import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; -import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; -import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; @@ -45,22 +41,22 @@ public abstract class TaskProcessor { @Autowired private TaskProcessorQueueFactory queueFactory; + @Autowired + private JobStatsService statsService; private QueueConsumerManager> taskConsumer; - private TbQueueProducer> taskResultProducer; private ExecutorService consumerExecutor; @PostConstruct public void init() { consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-consumer")); taskConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition - .name(getJobType().name() + "-tasks") + .name(getJobType().name().toLowerCase() + "-tasks") .msgPackProcessor(this::processMsgs) .pollInterval(125) .consumerCreator(() -> queueFactory.createTaskConsumer(getJobType())) .consumerExecutor(consumerExecutor) .build(); - taskResultProducer = queueFactory.createTaskResultProducer(); } @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @@ -92,7 +88,7 @@ public abstract class TaskProcessor { reportSuccess(task); } catch (Exception e) { log.error("Failed to process task (attempt {}): {}", task.getAttempt(), task, e); - if (task.getAttempt() < 3) { + if (task.getAttempt() <= task.getRetries()) { processTask(task); } else { reportFailure(task, e); @@ -102,34 +98,19 @@ public abstract class TaskProcessor { private void reportSuccess(Task task) { TaskResult result = TaskResult.builder() - .tenantId(task.getTenantId()) - .jobId(task.getJobId()) .success(true) .build(); - reportResult(result); + statsService.reportTaskResult(task.getJobId(), result); } private void reportFailure(Task task, Throwable error) { TaskResult result = TaskResult.builder() - .tenantId(task.getTenantId()) - .jobId(task.getJobId()) .failure(TaskFailure.builder() .error(error.getMessage()) .task(task) .build()) .build(); - reportResult(result); - } - - private void reportResult(TaskResult result) { - log.info("Reporting result: {}", result); - TaskResultProto resultProto = TaskResultProto.newBuilder() - .setValue(JacksonUtil.toString(result)) - .build(); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(result.getJobId().getId(), resultProto); - taskResultProducer.send(TopicPartitionInfo.builder() - .topic(taskResultProducer.getDefaultTopic()) - .build(), msg, TbQueueCallback.EMPTY); + statsService.reportTaskResult(task.getJobId(), result); } protected abstract void process(T task) throws Exception; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 1d504b0ab2..707dcdc3a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -745,6 +745,7 @@ public class ModelConstants { public static final String JOB_TABLE_NAME = "job"; public static final String JOB_TYPE_PROPERTY = "type"; public static final String JOB_KEY_PROPERTY = "key"; + public static final String JOB_DESCRIPTION_PROPERTY = "description"; public static final String JOB_STATUS_PROPERTY = "status"; public static final String JOB_CONFIGURATION_PROPERTY = "configuration"; public static final String JOB_RESULT_PROPERTY = "result"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java index 6e8b3e7958..d13c4bbed3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java @@ -56,6 +56,9 @@ public class JobEntity extends BaseSqlEntity { @Column(name = ModelConstants.JOB_KEY_PROPERTY, nullable = false) private String key; + @Column(name = ModelConstants.JOB_DESCRIPTION_PROPERTY, nullable = false) + private String description; + @Enumerated(EnumType.STRING) @Column(name = ModelConstants.JOB_STATUS_PROPERTY, nullable = false) private JobStatus status; @@ -74,6 +77,7 @@ public class JobEntity extends BaseSqlEntity { this.tenantId = getTenantUuid(job.getTenantId()); this.type = job.getType(); this.key = job.getKey(); + this.description = job.getDescription(); this.status = job.getStatus(); this.configuration = toJson(job.getConfiguration()); this.result = toJson(job.getResult()); @@ -87,6 +91,7 @@ public class JobEntity extends BaseSqlEntity { job.setTenantId(getTenantId(tenantId)); job.setType(type); job.setKey(key); + job.setDescription(description); job.setStatus(status); job.setConfiguration(fromJson(configuration, JobConfiguration.class)); job.setResult(fromJson(result, JobResult.class)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java index 273d8ef78a..9fff4d06b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java @@ -23,14 +23,22 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.dao.model.sql.JobEntity; +import java.util.List; import java.util.UUID; @Repository public interface JobRepository extends JpaRepository { - Page findByTenantId(UUID tenantId, Pageable pageable); + @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId " + + "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + + "OR ilike(j.description, concat('%', :searchText, '%')) = true)") + Page findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); @Modifying @Transactional @@ -45,7 +53,8 @@ public interface JobRepository extends JpaRepository { RETURNING ((result->>'successfulCount')::int + :count) + (result->>'failedCount')::int = (result->>'totalCount')::int """, nativeQuery = true) - boolean reportTaskSuccess(@Param("jobId") UUID jobId, @Param("count") int count); + boolean reportTaskSuccess(@Param("jobId") UUID jobId, + @Param("count") int count); @Modifying @Transactional @@ -64,6 +73,12 @@ public interface JobRepository extends JpaRepository { RETURNING ((result->>'failedCount')::int + 1) + (result->>'successfulCount')::int = (result->>'totalCount')::int """, nativeQuery = true) - boolean reportTaskFailure(@Param("jobId") UUID jobId, @Param("taskKey") String taskKey, @Param("error") String error); + boolean reportTaskFailure(@Param("jobId") UUID jobId, + @Param("taskKey") String taskKey, + @Param("error") String error); + + boolean existsByKeyAndStatusIn(String key, List statuses); + + boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java index d6286e2d77..b8b36dbc23 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.task; +import com.google.common.base.Strings; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; @@ -22,6 +23,8 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -30,6 +33,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.task.JobDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.Arrays; import java.util.UUID; @Component @@ -41,7 +45,7 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao @Override public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(jobRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + return DaoUtil.toPageData(jobRepository.findByTenantIdAndSearchText(tenantId.getId(), Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); } @Override @@ -54,6 +58,16 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao return jobRepository.reportTaskFailure(jobId.getId(), taskKey, error); } + @Override + public boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses) { + return jobRepository.existsByKeyAndStatusIn(key, Arrays.stream(statuses).toList()); + } + + @Override + public boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses) { + return jobRepository.existsByTenantIdAndTypeAndStatusIn(tenantId.getId(), type, Arrays.stream(statuses).toList()); + } + @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java index aa9e48600c..12d81c1c2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java @@ -22,13 +22,14 @@ import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; +import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; - -import java.util.List; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; @Service @RequiredArgsConstructor @@ -36,14 +37,21 @@ import java.util.List; public class DefaultJobService implements JobService { private final JobDao jobDao; + private final JobValidator validator = new JobValidator(); @Override public Job createJob(TenantId tenantId, Job job) { + validator.validate(job, Job::getTenantId); return jobDao.save(tenantId, job); } @Override - public void reportTaskResults(JobId jobId, List results) { + public Job findJobById(TenantId tenantId, JobId jobId) { + return jobDao.findById(tenantId, jobId.getId()); + } + + @Override + public void processStats(JobId jobId, JobStats jobStats) { Job job = jobDao.findById(TenantId.SYS_TENANT_ID, jobId.getId()); switch (job.getStatus()) { case PENDING -> { @@ -56,7 +64,11 @@ public class DefaultJobService implements JobService { } JobResult jobResult = job.getResult(); - for (TaskResult taskResult : results) { + if (jobStats.getTotalTasksCount() != null) { + jobResult.setTotalCount(jobStats.getTotalTasksCount()); + } + + for (TaskResult taskResult : jobStats.getTaskResults()) { if (taskResult.isSuccess()) { jobResult.setSuccessfulCount(jobResult.getSuccessfulCount() + 1); } else { @@ -67,7 +79,7 @@ public class DefaultJobService implements JobService { } } - if (jobResult.getSuccessfulCount() + jobResult.getFailedCount() >= jobResult.getTotalCount()) { + if (jobResult.getTotalCount() != null && jobResult.getSuccessfulCount() + jobResult.getFailedCount() >= jobResult.getTotalCount()) { if (jobResult.getFailures().isEmpty()) { job.setStatus(JobStatus.COMPLETED); } else { @@ -83,4 +95,22 @@ public class DefaultJobService implements JobService { return jobDao.findByTenantId(tenantId, pageLink); } + // todo: cancellation, reprocessing + + public class JobValidator extends DataValidator { + + @Override + protected void validateCreate(TenantId tenantId, Job job) { + if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), JobStatus.PENDING, JobStatus.RUNNING)) { + throw new DataValidationException("Job of this type is already running"); + } + } + + @Override + protected Job validateUpdate(TenantId tenantId, Job job) { + throw new IllegalArgumentException("Job can't be updated externally"); + } + + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java index 6a5b002aea..5c3c5b977c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.task; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; @@ -30,4 +32,8 @@ public interface JobDao extends Dao { boolean reportTaskFailure(JobId jobId, String taskKey, String error); + boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses); + + boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses); + } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 7fa31da5fb..5afc9398d2 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -955,6 +955,7 @@ CREATE TABLE IF NOT EXISTS job ( tenant_id uuid NOT NULL, type varchar NOT NULL, key varchar NOT NULL, + description varchar NOT NULL, status varchar NOT NULL, configuration varchar(1000) NOT NULL, result jsonb From 64f35d2d7b568e61f7956e00364a25795dcb7db9 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 25 Apr 2025 11:16:05 +0300 Subject: [PATCH 03/44] fixed entity service regisrty test --- .../thingsboard/server/dao/task/JobService.java | 3 ++- .../server/dao/task/DefaultJobService.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java index 5581f1eac7..a28b0e391a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java @@ -21,8 +21,9 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.EntityDaoService; -public interface JobService { +public interface JobService extends EntityDaoService { Job createJob(TenantId tenantId, Job job); diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java index 12d81c1c2c..dba569daa8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java @@ -18,6 +18,9 @@ package org.thingsboard.server.dao.task; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -31,6 +34,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import java.util.Optional; + @Service @RequiredArgsConstructor @Slf4j @@ -113,4 +118,14 @@ public class DefaultJobService implements JobService { } + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findJobById(tenantId, new JobId(entityId.getId()))); + } + + @Override + public EntityType getEntityType() { + return EntityType.JOB; + } + } From ee9237c4160ced8d5333148418d7c762035ca44c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 25 Apr 2025 12:21:20 +0300 Subject: [PATCH 04/44] Implement job cancellation --- .../controller/CalculatedFieldController.java | 3 +- .../server/controller/JobController.java | 12 +++ .../entitiy/EntityStateSourcingListener.java | 14 +++- .../server/service/job/DefaultJobManager.java | 29 ++++--- .../server/service/job/JobManager.java | 4 + .../queue/DefaultTbClusterService.java | 1 + .../server/service/job/JobManagerTest.java | 81 +++++++++++++++++-- .../server/dao/task/JobService.java | 4 +- .../common/data/job/JobConfiguration.java | 4 +- .../server/common/data/job/JobResult.java | 12 ++- .../server/common/data/job/JobStats.java | 2 + .../server/common/data/job/TaskResult.java | 1 + common/proto/src/main/proto/queue.proto | 15 +++- .../server/queue/task/JobStatsService.java | 15 ++-- .../server/queue/task/TaskProcessor.java | 61 +++++++++++--- .../server/dao/sql/task/JobRepository.java | 6 ++ .../server/dao/sql/task/JpaJobDao.java | 9 +-- .../server/dao/task/DefaultJobService.java | 63 ++++++++++++--- .../thingsboard/server/dao/task/JobDao.java | 4 +- 19 files changed, 280 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index f899d0f480..e5641b6748 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -97,7 +97,8 @@ public class CalculatedFieldController extends BaseController { public static final int TIMEOUT = 20; - private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" + private static final String TEST_SCRIPT_EXPRESSION = + "Execute the Script expression and return the result. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START + "{\n" + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index f7bc5255d3..d315a522ae 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.task.JobService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.job.JobManager; import java.util.UUID; @@ -47,10 +49,12 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERT public class JobController extends BaseController { private final JobService jobService; + private final JobManager jobManager; @GetMapping("/job/{id}") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") public Job getJobById(@PathVariable UUID id) throws ThingsboardException { + // todo check permissions return jobService.findJobById(getTenantId(), new JobId(id)); } @@ -66,8 +70,16 @@ public class JobController extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + // todo check permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return jobService.findJobsByTenantId(getTenantId(), pageLink); } + @PostMapping("/job/{id}/cancel") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void cancelJob(@PathVariable UUID id) throws ThingsboardException { + // todo check permissions + jobManager.cancelJob(getTenantId(), new JobId(id)); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 648e89adc9..68fb0bb7cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -134,6 +135,9 @@ public class EntityStateSourcingListener { case CALCULATED_FIELD -> { onCalculatedFieldUpdate(event.getEntity(), event.getOldEntity()); } + case JOB -> { + onJobUpdate((Job) event.getEntity()); + } default -> { } } @@ -212,8 +216,8 @@ public class EntityStateSourcingListener { public void handleEvent(ActionEntityEvent event) { log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) && - EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && event.getEntity() instanceof DeviceCredentials) { + EntityType.DEVICE.equals(event.getEntityId().getEntityType()) + && event.getEntity() instanceof DeviceCredentials) { tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { @@ -295,6 +299,12 @@ public class EntityStateSourcingListener { tbClusterService.onCalculatedFieldUpdated(calculatedField, oldCalculatedField, TbQueueCallback.EMPTY); } + private void onJobUpdate(Job job) { + if (job.getResult().getCancellationTs() > 0) { + tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); + } + } + private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { String data = JacksonUtil.toString(JacksonUtil.valueToTree(assignedDevice)); if (data != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 64c1615310..727beaf859 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobType; @@ -62,7 +63,7 @@ public class DefaultJobManager implements JobManager { private final JobStatsService jobStatsService; private final Map jobProcessors; private final Map>> taskProducers; - private final QueueConsumerManager> taskResultConsumer; + private final QueueConsumerManager> jobStatsConsumer; private final ExecutorService consumerExecutor; @Value("${queue.tasks.stats.processing_interval_ms:5000}") @@ -73,8 +74,8 @@ public class DefaultJobManager implements JobManager { this.jobStatsService = jobStatsService; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); - this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("task-result-consumer")); - this.taskResultConsumer = QueueConsumerManager.>builder() + this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("job-stats-consumer")); + this.jobStatsConsumer = QueueConsumerManager.>builder() .name("job-stats") .msgPackProcessor(this::processStats) .pollInterval(125) @@ -85,8 +86,8 @@ public class DefaultJobManager implements JobManager { @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void afterStartUp() { - taskResultConsumer.subscribe(); - taskResultConsumer.launch(); + jobStatsConsumer.subscribe(); + jobStatsConsumer.launch(); } @Override @@ -95,10 +96,16 @@ public class DefaultJobManager implements JobManager { log.info("Submitting job: {}", job); int tasksCount = jobProcessors.get(job.getType()).process(job, this::submitTask); - jobStatsService.reportAllTasksSubmitted(job.getId(), tasksCount); + jobStatsService.reportAllTasksSubmitted(job.getTenantId(), job.getId(), tasksCount); return job; } + @Override + public void cancelJob(TenantId tenantId, JobId jobId) { + log.info("Cancelling job: {}", jobId); + jobService.cancelJob(tenantId, jobId); + } + private void submitTask(Task task) { log.info("Submitting task: {}", task); TaskProto taskProto = TaskProto.newBuilder() @@ -126,8 +133,9 @@ public class DefaultJobManager implements JobManager { for (TbProtoQueueMsg msg : msgs) { JobStatsMsg statsMsg = msg.getValue(); + TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); JobId jobId = new JobId(new UUID(statsMsg.getJobIdMSB(), statsMsg.getJobIdLSB())); - JobStats jobStats = stats.computeIfAbsent(jobId, JobStats::new); + JobStats jobStats = stats.computeIfAbsent(jobId, __ -> new JobStats(tenantId, jobId)); if (statsMsg.hasTaskResult()) { TaskResult taskResult = JacksonUtil.fromString(statsMsg.getTaskResult().getValue(), TaskResult.class); @@ -140,8 +148,9 @@ public class DefaultJobManager implements JobManager { stats.forEach((jobId, jobStats) -> { try { - log.info("[{}] Processing job stats: {}", jobId, stats); - jobService.processStats(jobId, jobStats); + TenantId tenantId = jobStats.getTenantId(); + log.info("[{}][{}] Processing job stats: {}", tenantId, jobId, stats); + jobService.processStats(tenantId, jobId, jobStats); } catch (Exception e) { log.warn("Failed to process job stats for {}: {}", jobId, jobStats, e); } @@ -153,7 +162,7 @@ public class DefaultJobManager implements JobManager { @PreDestroy private void destroy() { - taskResultConsumer.stop(); + jobStatsConsumer.stop(); consumerExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java index c78de2a5d9..71ff3dcaa2 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.service.job; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; public interface JobManager { Job submitJob(Job job); + void cancelJob(TenantId tenantId, JobId jobId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 561dc7122a..cdc7abb4d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -594,6 +594,7 @@ public class DefaultTbClusterService implements TbClusterService { || entityType.equals(EntityType.ENTITY_VIEW) || entityType.equals(EntityType.NOTIFICATION_RULE) || entityType.equals(EntityType.CALCULATED_FIELD) + || entityType.equals(EntityType.JOB) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index f9ee2e2b13..68d2d96471 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -20,22 +20,28 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.job.task.DummyTaskProcessor; import java.util.List; 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.Mockito.doAnswer; @DaoSqlTest @TestPropertySource(properties = { @@ -46,6 +52,9 @@ public class JobManagerTest extends AbstractControllerTest { @Autowired private JobManager jobManager; + @SpyBean + private DummyTaskProcessor taskProcessor; + @Before public void setUp() throws Exception { loginTenantAdmin(); @@ -80,6 +89,7 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); assertThat(job.getResult().getFailures()).isEmpty(); + assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); } @@ -104,15 +114,76 @@ public class JobManagerTest extends AbstractControllerTest { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); - assertThat(job.getResult().getSuccessfulCount()).isEqualTo(successfulTasks); - assertThat(job.getResult().getFailedCount()).isEqualTo(failedTasks); - assertThat(job.getResult().getTotalCount()).isEqualTo(successfulTasks + failedTasks); - assertThat(job.getResult().getFailures().get("Task 1")).isEqualTo("error3"); // last error - assertThat(job.getResult().getFailures().get("Task 2")).isEqualTo("error3"); // last error + JobResult jobResult = job.getResult(); + assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); + assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); + assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks); + assertThat(jobResult.getFailures().get("Task 1")).isEqualTo("error3"); // last error + assertThat(jobResult.getFailures().get("Task 2")).isEqualTo("error3"); // last error + assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); + }); + } + + @Test + public void testCancelJob_whileRunning() throws Exception { + int tasksCount = 100; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(100) + .build()) + .build()).getId(); + + Thread.sleep(500); + jobManager.cancelJob(tenantId, jobId); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); + assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getCancelledCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); } + @Test + public void testCancelJob_simulateTaskProcessorRestart() { + int tasksCount = 10; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(100) + .build()) + .build()).getId(); + + // simulate cancelled jobs are forgotten + AtomicInteger cancellationRenotifyAttempt = new AtomicInteger(0); + doAnswer(inv -> { + if (cancellationRenotifyAttempt.incrementAndGet() >= 5) { + inv.callRealMethod(); + } + return null; + }).when(taskProcessor).addToCancelledJobs(any()); // ignoring cancellation event, + jobManager.cancelJob(tenantId, jobId); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + System.err.println(job); + assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); + assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getCancelledCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); + }); + } private Job findJobById(JobId jobId) throws Exception { return doGet("/api/job/" + jobId, Job.class); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java index a28b0e391a..9ce802b84f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java @@ -29,7 +29,9 @@ public interface JobService extends EntityDaoService { Job findJobById(TenantId tenantId, JobId jobId); - void processStats(JobId jobId, JobStats jobStats); + void cancelJob(TenantId tenantId, JobId jobId); + + void processStats(TenantId tenantId, JobId jobId, JobStats jobStats); PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index eccdfae6bb..b541458289 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -20,13 +20,15 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "CF_REPROCESSING", value = CfReprocessingJobConfiguration.class), @Type(name = "DUMMY", value = DummyJobConfiguration.class), }) -public interface JobConfiguration { +public interface JobConfiguration extends Serializable { JobType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 07b6c4eadd..abaa86facb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -22,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import lombok.NoArgsConstructor; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; @@ -33,13 +35,21 @@ import java.util.Map; }) @Data @NoArgsConstructor -public abstract class JobResult { +public abstract class JobResult implements Serializable { private int successfulCount; private int failedCount; + private int cancelledCount; private Integer totalCount = null; // set when all tasks are submitted private Map failures = new HashMap<>(); + private long cancellationTs; + + @JsonIgnore + public int getCompletedCount() { + return successfulCount + failedCount + cancelledCount; + } + public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java index 6491d0998a..9d2c3d9be2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java @@ -17,12 +17,14 @@ package org.thingsboard.server.common.data.job; import lombok.Data; import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; import java.util.ArrayList; import java.util.List; @Data public class JobStats { + private final TenantId tenantId; private final JobId jobId; private final List taskResults = new ArrayList<>(); private Integer totalTasksCount; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java index ae5e6bbba9..57f2f44d7a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java @@ -27,6 +27,7 @@ import lombok.NoArgsConstructor; public class TaskResult { private boolean success; + private boolean cancelled; private TaskFailure failure; @Data diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 032301206f..b4d436a32a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -61,6 +61,7 @@ enum EntityTypeProto { MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; + JOB = 41; } enum ApiUsageRecordKeyProto { @@ -534,6 +535,10 @@ message ToEdqsCoreServiceMsg { bytes value = 1; } +message ToJobManagerMsg { + bytes value = 1; +} + message LwM2MRegistrationRequestMsg { string tenantId = 1; string endpoint = 2; @@ -1852,10 +1857,12 @@ message TaskProto { } message JobStatsMsg { - int64 jobIdMSB = 1; - int64 jobIdLSB = 2; - optional TaskResultProto taskResult = 3; - optional int32 totalTasksCount = 4; + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 jobIdMSB = 3; + int64 jobIdLSB = 4; + optional TaskResultProto taskResult = 5; + optional int32 totalTasksCount = 6; } message TaskResultProto { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java index 6c36c573ca..ceba2645f9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; @@ -38,21 +39,23 @@ public class JobStatsService { private final TbQueueProducerProvider producerProvider; - public void reportTaskResult(JobId jobId, TaskResult result) { - report(jobId, JobStatsMsg.newBuilder() + public void reportTaskResult(TenantId tenantId, JobId jobId, TaskResult result) { + report(tenantId, jobId, JobStatsMsg.newBuilder() .setTaskResult(TaskResultProto.newBuilder() .setValue(JacksonUtil.toString(result)) .build())); } - public void reportAllTasksSubmitted(JobId jobId, int tasksCount) { - report(jobId, JobStatsMsg.newBuilder() + public void reportAllTasksSubmitted(TenantId tenantId, JobId jobId, int tasksCount) { + report(tenantId, jobId, JobStatsMsg.newBuilder() .setTotalTasksCount(tasksCount)); } - private void report(JobId jobId, JobStatsMsg.Builder statsMsg) { + private void report(TenantId tenantId, JobId jobId, JobStatsMsg.Builder statsMsg) { log.info("[{}] Reporting: {}", jobId, statsMsg); - statsMsg.setJobIdMSB(jobId.getId().getMostSignificantBits()) + statsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setJobIdMSB(jobId.getId().getMostSignificantBits()) .setJobIdLSB(jobId.getId().getLeastSignificantBits()); TbProtoQueueMsg msg = new TbProtoQueueMsg<>(jobId.getId(), statsMsg.build()); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 52fd382c3d..43d2dd74c0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -17,14 +17,20 @@ package org.thingsboard.server.queue.task; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -33,12 +39,16 @@ import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; 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; -@Slf4j public abstract class TaskProcessor { + protected final Logger log = LoggerFactory.getLogger(getClass()); + @Autowired private TaskProcessorQueueFactory queueFactory; @Autowired @@ -47,12 +57,14 @@ public abstract class TaskProcessor { private QueueConsumerManager> taskConsumer; private ExecutorService consumerExecutor; + private final Set cancelledJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine + @PostConstruct public void init() { consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-consumer")); taskConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition .name(getJobType().name().toLowerCase() + "-tasks") - .msgPackProcessor(this::processMsgs) + .msgPackProcessor(this::processMsgs) // todo: max.poll.records = 1 .pollInterval(125) .consumerCreator(() -> queueFactory.createTaskConsumer(getJobType())) .consumerExecutor(consumerExecutor) @@ -65,16 +77,27 @@ public abstract class TaskProcessor { taskConsumer.launch(); } - @PreDestroy - public void destroy() { - taskConsumer.stop(); - consumerExecutor.shutdownNow(); + @EventListener + public void onJobCancelled(ComponentLifecycleMsg event) { + if (event.getEntityId().getEntityType() != EntityType.JOB) { + return; + } + JobId jobId = (JobId) event.getEntityId(); + if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { + log.info("Adding job {} to cancelled", jobId); + addToCancelledJobs(jobId); + } } private void processMsgs(List> msgs, TbQueueConsumer> consumer) { for (TbProtoQueueMsg msg : msgs) { TaskProto taskProto = msg.getValue(); Task task = JacksonUtil.fromString(taskProto.getValue(), Task.class); + if (cancelledJobs.contains(task.getJobId().getId())) { + log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); + reportCancelled(task); + continue; + } processTask((T) task); } consumer.commit(); @@ -96,11 +119,13 @@ public abstract class TaskProcessor { } } + protected abstract void process(T task) throws Exception; + private void reportSuccess(Task task) { TaskResult result = TaskResult.builder() .success(true) .build(); - statsService.reportTaskResult(task.getJobId(), result); + statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } private void reportFailure(Task task, Throwable error) { @@ -110,10 +135,26 @@ public abstract class TaskProcessor { .task(task) .build()) .build(); - statsService.reportTaskResult(task.getJobId(), result); + statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); + } + + private void reportCancelled(Task task) { + TaskResult result = TaskResult.builder() + .cancelled(true) + .build(); + statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); + } + + public void addToCancelledJobs(JobId jobId) { + cancelledJobs.add(jobId.getId()); + } + + @PreDestroy + public void destroy() { + taskConsumer.stop(); + consumerExecutor.shutdownNow(); } - protected abstract void process(T task) throws Exception; public abstract JobType getJobType(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java index 9fff4d06b7..472df05bdd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.dao.sql.task; +import jakarta.persistence.LockModeType; import jakarta.transaction.Transactional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -40,6 +42,10 @@ public interface JobRepository extends JpaRepository { @Param("searchText") String searchText, Pageable pageable); + @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE + @Query("SELECT j FROM JobEntity j WHERE j.id = :id") + JobEntity findByIdForUpdate(UUID id); + @Modifying @Transactional @Query(value = """ diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java index b8b36dbc23..92b9fc8a72 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java @@ -49,13 +49,8 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao } @Override - public boolean reportTaskSuccess(JobId jobId, int tasksCount) { - return jobRepository.reportTaskSuccess(jobId.getId(), tasksCount); - } - - @Override - public boolean reportTaskFailure(JobId jobId, String taskKey, String error) { - return jobRepository.reportTaskFailure(jobId.getId(), taskKey, error); + public Job findByIdForUpdate(TenantId tenantId, JobId jobId) { + return DaoUtil.getData(jobRepository.findByIdForUpdate(jobId.getId())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java index dba569daa8..1e694508fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -39,7 +42,7 @@ import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j -public class DefaultJobService implements JobService { +public class DefaultJobService extends AbstractEntityService implements JobService { private final JobDao jobDao; private final JobValidator validator = new JobValidator(); @@ -47,7 +50,7 @@ public class DefaultJobService implements JobService { @Override public Job createJob(TenantId tenantId, Job job) { validator.validate(job, Job::getTenantId); - return jobDao.save(tenantId, job); + return saveJob(tenantId, job, false); } @Override @@ -55,9 +58,21 @@ public class DefaultJobService implements JobService { return jobDao.findById(tenantId, jobId.getId()); } + @Transactional @Override - public void processStats(JobId jobId, JobStats jobStats) { - Job job = jobDao.findById(TenantId.SYS_TENANT_ID, jobId.getId()); + public void cancelJob(TenantId tenantId, JobId jobId) { + Job job = findForUpdate(tenantId, jobId); + if (job.getStatus() != JobStatus.PENDING && job.getStatus() != JobStatus.RUNNING) { + throw new IllegalArgumentException("Job already " + job.getStatus().name().toLowerCase()); + } + job.getResult().setCancellationTs(System.currentTimeMillis()); + saveJob(tenantId, job, true); + } + + @Transactional + @Override + public void processStats(TenantId tenantId, JobId jobId, JobStats jobStats) { + Job job = findForUpdate(tenantId, jobId); switch (job.getStatus()) { case PENDING -> { job.setStatus(JobStatus.RUNNING); @@ -73,26 +88,52 @@ public class DefaultJobService implements JobService { jobResult.setTotalCount(jobStats.getTotalTasksCount()); } + boolean publishEvent = false; for (TaskResult taskResult : jobStats.getTaskResults()) { if (taskResult.isSuccess()) { jobResult.setSuccessfulCount(jobResult.getSuccessfulCount() + 1); + } else if (taskResult.isCancelled()) { + jobResult.setCancelledCount(jobResult.getCancelledCount() + 1); } else { TaskFailure failure = taskResult.getFailure(); String key = failure.getTask().getKey(); jobResult.setFailedCount(jobResult.getFailedCount() + 1); jobResult.getFailures().put(key, failure.getError()); } + + if (jobResult.getCancellationTs() > 0) { + if (!taskResult.isCancelled() && System.currentTimeMillis() > jobResult.getCancellationTs()) { + log.info("Got task result for cancelled job {}: {}, re-notifying processors about cancellation", jobId, taskResult); + // task processor forgot the task is cancelled + publishEvent = true; + } + } } - if (jobResult.getTotalCount() != null && jobResult.getSuccessfulCount() + jobResult.getFailedCount() >= jobResult.getTotalCount()) { - if (jobResult.getFailures().isEmpty()) { - job.setStatus(JobStatus.COMPLETED); - } else { + if (jobResult.getTotalCount() != null && jobResult.getCompletedCount() >= jobResult.getTotalCount()) { + if (jobResult.getCancellationTs() > 0) { + job.setStatus(JobStatus.CANCELLED); + } else if (jobResult.getFailedCount() > 0) { job.setStatus(JobStatus.FAILED); + } else { + job.setStatus(JobStatus.COMPLETED); } } log.info("Saving job {}", job); - jobDao.save(TenantId.SYS_TENANT_ID, job); + saveJob(tenantId, job, publishEvent); + } + + private Job saveJob(TenantId tenantId, Job job, boolean publishEvent) { + job = jobDao.save(tenantId, job); + if (publishEvent) { + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(tenantId) + .entityId(job.getId()) + .entity(job) + .created(false) + .build()); + } + return job; } @Override @@ -100,6 +141,10 @@ public class DefaultJobService implements JobService { return jobDao.findByTenantId(tenantId, pageLink); } + private Job findForUpdate(TenantId tenantId, JobId jobId) { + return jobDao.findByIdForUpdate(tenantId, jobId); + } + // todo: cancellation, reprocessing public class JobValidator extends DataValidator { diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java index 5c3c5b977c..3b1d6631dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java @@ -28,9 +28,7 @@ public interface JobDao extends Dao { PageData findByTenantId(TenantId tenantId, PageLink pageLink); - boolean reportTaskSuccess(JobId jobId, int tasksCount); - - boolean reportTaskFailure(JobId jobId, String taskKey, String error); + Job findByIdForUpdate(TenantId tenantId, JobId jobId); boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses); From a317b4707a7aa504b994ca9e5e5bb66ed54fe721 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 25 Apr 2025 17:40:49 +0300 Subject: [PATCH 05/44] Discard tasks when tenant is deleted --- .../job/task/CfReprocessingTaskProcessor.java | 2 +- .../service/job/task/DummyTaskProcessor.java | 2 +- .../server/service/job/JobManagerTest.java | 39 ++++++++++++ .../server/queue/task/TaskProcessor.java | 61 ++++++++++++------- .../server/dao/task/DefaultJobService.java | 13 +++- .../server/dao/tenant/TenantServiceImpl.java | 2 +- 6 files changed, 92 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java index 36899516f9..5d4005307c 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java @@ -33,7 +33,7 @@ public class CfReprocessingTaskProcessor extends TaskProcessor future = SettableFuture.create(); cfReprocessingService.reprocess(task, new TbCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 10aafc197d..dc4a193bb9 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -26,7 +26,7 @@ import org.thingsboard.server.queue.task.TaskProcessor; public class DummyTaskProcessor extends TaskProcessor { @Override - protected void process(DummyTask task) throws Exception { + public void process(DummyTask task) throws Exception { if (task.getProcessingTimeMs() > 0) { Thread.sleep(task.getProcessingTimeMs()); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 68d2d96471..b01d0099df 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -16,13 +16,16 @@ package org.thingsboard.server.service.job; import com.fasterxml.jackson.core.type.TypeReference; +import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; @@ -32,6 +35,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.service.job.task.DummyTaskProcessor; import java.util.List; @@ -42,6 +47,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; @DaoSqlTest @TestPropertySource(properties = { @@ -52,9 +59,15 @@ public class JobManagerTest extends AbstractControllerTest { @Autowired private JobManager jobManager; + @Autowired + private JobService jobService; + @SpyBean private DummyTaskProcessor taskProcessor; + @SpyBean + private JobStatsService jobStatsService; + @Before public void setUp() throws Exception { loginTenantAdmin(); @@ -185,6 +198,32 @@ public class JobManagerTest extends AbstractControllerTest { }); } + @Test + public void whenTenantIsDeleted_thenCancelAllTheJobs() throws Exception { + loginSysAdmin(); + createDifferentTenant(); + + TenantId tenantId = this.differentTenantId; + jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(1000) + .taskProcessingTimeMs(500) + .build()) + .build()); + + Thread.sleep(2000); + deleteDifferentTenant(); + Mockito.reset(jobStatsService); + + Thread.sleep(3000); + verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); + Assertions.assertThat(jobService.findJobsByTenantId(tenantId, new PageLink(100, 0)).getData()).isEmpty(); + } + private Job findJobById(JobId jobId) throws Exception { return doGet("/api/job/" + jobId, Job.class); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 43d2dd74c0..2cf0f22b90 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -23,8 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; @@ -57,6 +56,7 @@ public abstract class TaskProcessor { private QueueConsumerManager> taskConsumer; private ExecutorService consumerExecutor; + private final Set deletedTenants = ConcurrentHashMap.newKeySet(); private final Set cancelledJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine @PostConstruct @@ -78,37 +78,54 @@ public abstract class TaskProcessor { } @EventListener - public void onJobCancelled(ComponentLifecycleMsg event) { - if (event.getEntityId().getEntityType() != EntityType.JOB) { - return; - } - JobId jobId = (JobId) event.getEntityId(); - if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { - log.info("Adding job {} to cancelled", jobId); - addToCancelledJobs(jobId); + public void onComponentLifecycle(ComponentLifecycleMsg event) { + EntityId entityId = event.getEntityId(); + switch (entityId.getEntityType()) { + case JOB -> { + if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { + log.info("Adding job {} to cancelledJobs", entityId); + addToCancelledJobs(entityId.getId()); + } + } + case TENANT -> { + if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + deletedTenants.add(entityId.getId()); + log.info("Adding tenant {} to deletedTenants", entityId); + } + } } } - private void processMsgs(List> msgs, TbQueueConsumer> consumer) { + private void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { for (TbProtoQueueMsg msg : msgs) { - TaskProto taskProto = msg.getValue(); - Task task = JacksonUtil.fromString(taskProto.getValue(), Task.class); - if (cancelledJobs.contains(task.getJobId().getId())) { - log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); - reportCancelled(task); - continue; + try { + Task task = JacksonUtil.fromString(msg.getValue().getValue(), Task.class); + if (cancelledJobs.contains(task.getJobId().getId())) { + log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); + reportCancelled(task); + continue; + } else if (deletedTenants.contains(task.getTenantId().getId())) { + log.info("Skipping task '{}' for deleted tenant {}", task.getKey(), task.getTenantId()); + continue; + } + processTask((T) task); + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + log.error("Failed to process msg: {}", msg, e); } - processTask((T) task); } consumer.commit(); } - private void processTask(T task) { + private void processTask(T task) throws Exception { // todo: timeout and task interruption task.setAttempt(task.getAttempt() + 1); log.info("Processing task: {}", task); try { process(task); reportSuccess(task); + } catch (InterruptedException e) { + throw e; } catch (Exception e) { log.error("Failed to process task (attempt {}): {}", task.getAttempt(), task, e); if (task.getAttempt() <= task.getRetries()) { @@ -119,7 +136,7 @@ public abstract class TaskProcessor { } } - protected abstract void process(T task) throws Exception; + public abstract void process(T task) throws Exception; private void reportSuccess(Task task) { TaskResult result = TaskResult.builder() @@ -145,8 +162,8 @@ public abstract class TaskProcessor { statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } - public void addToCancelledJobs(JobId jobId) { - cancelledJobs.add(jobId.getId()); + public void addToCancelledJobs(UUID jobId) { + cancelledJobs.add(jobId); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java index 1e694508fa..16b4397d87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java @@ -18,10 +18,10 @@ package org.thingsboard.server.dao.task; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -73,6 +73,10 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public void processStats(TenantId tenantId, JobId jobId, JobStats jobStats) { Job job = findForUpdate(tenantId, jobId); + if (job == null) { + log.info("Got stale stats for job {}: {}", jobId, jobStats); + return; + } switch (job.getStatus()) { case PENDING -> { job.setStatus(JobStatus.RUNNING); @@ -165,7 +169,12 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return Optional.ofNullable(findJobById(tenantId, new JobId(entityId.getId()))); + return Optional.ofNullable(findJobById(tenantId, (JobId) entityId)); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + jobDao.removeById(tenantId, id.getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 1dbca5af12..de28d047e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -172,7 +172,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Mon, 28 Apr 2025 17:24:14 +0300 Subject: [PATCH 06/44] Multiple queued jobs of the same type --- .../server/controller/JobController.java | 2 +- .../entitiy/EntityStateSourcingListener.java | 7 +- .../job/CfReprocessingJobProcessor.java | 7 +- .../server/service/job/DefaultJobManager.java | 48 +++++-- .../server/service/job/DummyJobProcessor.java | 11 +- .../server/service/job/JobManager.java | 2 + .../server/service/job/JobProcessor.java | 8 +- .../server/service/job/JobManagerTest.java | 119 +++++++++++++++++- .../src/test/resources/logback-test.xml | 2 +- .../server/dao/{task => job}/JobService.java | 4 +- .../common/data/job/CfReprocessingTask.java | 2 + .../data/job/DummyJobConfiguration.java | 3 + .../server/common/data/job/DummyTask.java | 2 + .../server/common/data/job/Job.java | 3 +- .../server/common/data/job/JobResult.java | 5 +- .../server/common/data/job/JobStatus.java | 16 ++- .../server/common/data/job/TaskResult.java | 2 +- .../server/queue/task/JobStatsService.java | 2 +- .../server/queue/task/TaskProcessor.java | 16 +-- .../dao/{task => job}/DefaultJobService.java | 119 ++++++++++++------ .../server/dao/{task => job}/JobDao.java | 4 +- .../dao/sql/{task => job}/JobRepository.java | 49 ++------ .../dao/sql/{task => job}/JpaJobDao.java | 10 +- 23 files changed, 314 insertions(+), 129 deletions(-) rename common/dao-api/src/main/java/org/thingsboard/server/dao/{task => job}/JobService.java (92%) rename dao/src/main/java/org/thingsboard/server/dao/{task => job}/DefaultJobService.java (54%) rename dao/src/main/java/org/thingsboard/server/dao/{task => job}/JobDao.java (90%) rename dao/src/main/java/org/thingsboard/server/dao/sql/{task => job}/JobRepository.java (56%) rename dao/src/main/java/org/thingsboard/server/dao/sql/{task => job}/JpaJobDao.java (87%) diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index d315a522ae..5718d6e388 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.job.JobManager; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 68fb0bb7cf..f70354c3a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -59,6 +60,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.service.job.JobManager; import java.util.Set; @@ -70,6 +72,7 @@ public class EntityStateSourcingListener { private final TenantService tenantService; private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; + private final JobManager jobManager; @PostConstruct public void init() { @@ -300,7 +303,9 @@ public class EntityStateSourcingListener { } private void onJobUpdate(Job job) { - if (job.getResult().getCancellationTs() > 0) { + jobManager.onJobUpdate(job); + if (job.getResult().getCancellationTs() > 0 || job.getStatus().isOneOf(JobStatus.FAILED)) { + // task processors will add this job to the list of discarded tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); } } diff --git a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java index b5f6c4665f..edb38da5c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.job; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -36,15 +35,13 @@ import java.util.function.Consumer; @Component @RequiredArgsConstructor -public class CfReprocessingJobProcessor extends JobProcessor { +public class CfReprocessingJobProcessor implements JobProcessor { private final DeviceService deviceService; private final AssetService assetService; - // fixme: multiple jobs with single type - @Transactional @Override - public int process(Job job, Consumer taskConsumer) { + public int process(Job job, Consumer taskConsumer) throws Exception { CfReprocessingJobConfiguration configuration = job.getConfiguration(); CalculatedField calculatedField = configuration.getCalculatedField(); diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 727beaf859..6554bebe9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -18,19 +18,22 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobStats; +import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.task.JobService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueCallback; @@ -64,6 +67,7 @@ public class DefaultJobManager implements JobManager { private final Map jobProcessors; private final Map>> taskProducers; private final QueueConsumerManager> jobStatsConsumer; + private final ExecutorService executor; private final ExecutorService consumerExecutor; @Value("${queue.tasks.stats.processing_interval_ms:5000}") @@ -74,6 +78,7 @@ public class DefaultJobManager implements JobManager { this.jobStatsService = jobStatsService; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); + this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("job-stats-consumer")); this.jobStatsConsumer = QueueConsumerManager.>builder() .name("job-stats") @@ -92,22 +97,40 @@ public class DefaultJobManager implements JobManager { @Override public Job submitJob(Job job) { - job = jobService.createJob(job.getTenantId(), job); - log.info("Submitting job: {}", job); + log.debug("Submitting job: {}", job); + return jobService.createJob(job.getTenantId(), job); + } - int tasksCount = jobProcessors.get(job.getType()).process(job, this::submitTask); - jobStatsService.reportAllTasksSubmitted(job.getTenantId(), job.getId(), tasksCount); - return job; + @Override + public void onJobUpdate(Job job) { + if (job.getStatus() == JobStatus.PENDING) { + executor.execute(() -> { + TenantId tenantId = job.getTenantId(); + JobId jobId = job.getId(); + try { + int tasksCount = jobProcessors.get(job.getType()).process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted + log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount); + jobStatsService.reportAllTasksSubmitted(tenantId, jobId, tasksCount); + } catch (Throwable e) { + log.error("[{}][{}][{}] Failed to submit tasks", tenantId, jobId, job.getType(), e); + try { + jobService.markAsFailed(tenantId, jobId, ExceptionUtils.getStackTrace(e)); + } catch (Throwable e2) { + log.error("[{}][{}] Failed to mark job as failed", tenantId, jobId, e2); + } + } + }); + } } @Override public void cancelJob(TenantId tenantId, JobId jobId) { - log.info("Cancelling job: {}", jobId); + log.info("[{}][{}] Cancelling job", tenantId, jobId); jobService.cancelJob(tenantId, jobId); } private void submitTask(Task task) { - log.info("Submitting task: {}", task); + log.info("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); TaskProto taskProto = TaskProto.newBuilder() .setValue(JacksonUtil.toString(task)) .build(); @@ -147,22 +170,23 @@ public class DefaultJobManager implements JobManager { } stats.forEach((jobId, jobStats) -> { + TenantId tenantId = jobStats.getTenantId(); try { - TenantId tenantId = jobStats.getTenantId(); - log.info("[{}][{}] Processing job stats: {}", tenantId, jobId, stats); + log.debug("[{}][{}] Processing job stats: {}", tenantId, jobId, stats); jobService.processStats(tenantId, jobId, jobStats); } catch (Exception e) { - log.warn("Failed to process job stats for {}: {}", jobId, jobStats, e); + log.error("[{}][{}] Failed to process job stats: {}", tenantId, jobId, jobStats, e); } }); consumer.commit(); - Thread.sleep(statsProcessingInterval); + Thread.sleep(statsProcessingInterval); // todo: test with bigger interval } @PreDestroy private void destroy() { jobStatsConsumer.stop(); + executor.shutdownNow(); consumerExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java index bed8f3f25e..cda7201e1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -28,11 +28,18 @@ import java.util.function.Consumer; @Component @RequiredArgsConstructor -public class DummyJobProcessor extends JobProcessor { +public class DummyJobProcessor implements JobProcessor { @Override - public int process(Job job, Consumer taskConsumer) { + public int process(Job job, Consumer taskConsumer) throws Exception { DummyJobConfiguration configuration = job.getConfiguration(); + if (configuration.getGeneralError() != null) { + for (int number = 1; number <= configuration.getSubmittedTasksBeforeGeneralError(); number++) { + taskConsumer.accept(createTask(job, configuration, number, null)); + } + Thread.sleep(configuration.getTaskProcessingTimeMs() * (configuration.getSubmittedTasksBeforeGeneralError() / 2)); // sleeping so that some tasks are processed + throw new RuntimeException(configuration.getGeneralError()); + } for (int number = 1; number <= configuration.getSuccessfulTasksCount(); number++) { taskConsumer.accept(createTask(job, configuration, number, null)); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java index 71ff3dcaa2..3932361f3f 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java @@ -25,4 +25,6 @@ public interface JobManager { void cancelJob(TenantId tenantId, JobId jobId); + void onJobUpdate(Job job); + } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index 01f7291dd3..2431134e99 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -15,16 +15,16 @@ */ package org.thingsboard.server.service.job; -import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.Task; import java.util.function.Consumer; -public abstract class JobProcessor { +public interface JobProcessor { - public abstract int process(Job job, Consumer taskConsumer); + int process(Job job, Consumer taskConsumer) throws Exception; - public abstract JobType getType(); + JobType getType(); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index b01d0099df..173a528e14 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -33,12 +33,14 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.dao.task.JobService; import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.service.job.task.DummyTaskProcessor; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -157,7 +159,7 @@ public class JobManagerTest extends AbstractControllerTest { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); - assertThat(job.getResult().getCancelledCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); @@ -184,15 +186,14 @@ public class JobManagerTest extends AbstractControllerTest { inv.callRealMethod(); } return null; - }).when(taskProcessor).addToCancelledJobs(any()); // ignoring cancellation event, + }).when(taskProcessor).addToDiscardedJobs(any()); // ignoring cancellation event, jobManager.cancelJob(tenantId, jobId); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); - System.err.println(job); assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); - assertThat(job.getResult().getCancelledCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); @@ -224,12 +225,118 @@ public class JobManagerTest extends AbstractControllerTest { Assertions.assertThat(jobService.findJobsByTenantId(tenantId, new PageLink(100, 0)).getData()).isEmpty(); } + @Test + public void testSubmitMultipleJobs() { + int tasksCount = 3; + int jobsCount = 3; + for (int i = 1; i <= jobsCount; i++) { + Job job = Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job-" + i) + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) + .build()) + .build(); + jobManager.submitJob(job); + } + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + List jobs = findJobs(); + assertThat(jobs).hasSize(jobsCount); + Job firstJob = jobs.get(2); // ordered by createdTime descending + assertThat(firstJob.getStatus()).isEqualTo(JobStatus.RUNNING); + Job secondJob = jobs.get(1); + assertThat(secondJob.getStatus()).isEqualTo(JobStatus.QUEUED); + Job thirdJob = jobs.get(0); + assertThat(thirdJob.getStatus()).isEqualTo(JobStatus.QUEUED); + }); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + List jobs = findJobs(); + for (Job job : jobs) { + assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); + } + }); + } + + @Test + public void testCancelQueuedJob() { + int tasksCount = 3; + int jobsCount = 3; + List jobIds = new ArrayList<>(); + for (int i = 1; i <= jobsCount; i++) { + Job job = Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job-" + i) + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) + .build()) + .build(); + jobIds.add(jobManager.submitJob(job).getId()); + } + + for (int i = 1; i < jobIds.size(); i++) { + jobManager.cancelJob(tenantId, jobIds.get(i)); + } + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + List jobs = findJobs(); + + Job firstJob = jobs.get(2); + assertThat(firstJob.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(firstJob.getResult().getSuccessfulCount()).isEqualTo(tasksCount); + assertThat(firstJob.getResult().getTotalCount()).isEqualTo(tasksCount); + + Job secondJob = jobs.get(1); + assertThat(secondJob.getStatus()).isEqualTo(JobStatus.CANCELLED); + assertThat(secondJob.getResult().getCompletedCount()).isZero(); + + Job thirdJob = jobs.get(0); + assertThat(thirdJob.getStatus()).isEqualTo(JobStatus.CANCELLED); + assertThat(thirdJob.getResult().getCompletedCount()).isZero(); + }); + } + + @Test + public void testGeneralJobError() { + int submittedTasks = 100; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(submittedTasks) + .taskProcessingTimeMs(10) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + assertThat(job.getResult().getSuccessfulCount()).isBetween(1, submittedTasks); + assertThat(job.getResult().getDiscardedCount()).isBetween(1, submittedTasks); + assertThat(job.getResult().getTotalCount()).isNull(); + }); + } + + // todo: job with zero tasks, reprocessing + private Job findJobById(JobId jobId) throws Exception { return doGet("/api/job/" + jobId, Job.class); } private List findJobs() throws Exception { - return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0)).getData(); + return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } } \ No newline at end of file diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index a0efcf52c1..13c93da411 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,7 @@ - + diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java similarity index 92% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 9ce802b84f..62fd60eaac 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/task/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.task; +package org.thingsboard.server.dao.job; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -31,6 +31,8 @@ public interface JobService extends EntityDaoService { void cancelJob(TenantId tenantId, JobId jobId); + void markAsFailed(TenantId tenantId, JobId jobId, String error); + void processStats(TenantId tenantId, JobId jobId, JobStats jobStats); PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java index 5c380c4dfa..8846a6a12f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.job; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.id.EntityId; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @SuperBuilder +@ToString(callSuper = true) public class CfReprocessingTask extends Task { private CalculatedField calculatedField; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java index 7fe621c058..70daf6f9c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -34,6 +34,9 @@ public class DummyJobConfiguration implements JobConfiguration { private List errors; private int retries; + private String generalError; + private int submittedTasksBeforeGeneralError; + @Override public JobType getType() { return JobType.DUMMY; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java index dee97fc3c9..ae93ed06bb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.job; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import java.util.List; @@ -26,6 +27,7 @@ import java.util.List; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @SuperBuilder +@ToString(callSuper = true) public class DummyTask extends Task { private int number; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index 5a51a49239..e96d42cad1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -21,6 +21,7 @@ import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.JobId; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId; @Data @NoArgsConstructor +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class Job extends BaseData implements HasTenantId { @@ -51,7 +53,6 @@ public class Job extends BaseData implements HasTenantId { this.key = key; this.description = description; this.configuration = configuration; - this.status = JobStatus.PENDING; this.result = switch (type) { case CF_REPROCESSING -> new CfReprocessingJobResult(); case DUMMY -> new DummyJobResult(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index abaa86facb..b517877906 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -39,15 +39,16 @@ public abstract class JobResult implements Serializable { private int successfulCount; private int failedCount; - private int cancelledCount; + private int discardedCount; private Integer totalCount = null; // set when all tasks are submitted private Map failures = new HashMap<>(); + private String generalError; private long cancellationTs; @JsonIgnore public int getCompletedCount() { - return successfulCount + failedCount + cancelledCount; + return successfulCount + failedCount + discardedCount; } public abstract JobType getJobType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java index 026e19c5b2..17d050a540 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java @@ -16,9 +16,23 @@ package org.thingsboard.server.common.data.job; public enum JobStatus { + QUEUED, PENDING, RUNNING, COMPLETED, FAILED, - CANCELLED + CANCELLED; + + public boolean isOneOf(JobStatus... statuses) { + if (statuses == null) { + return false; + } + for (JobStatus status : statuses) { + if (this == status) { + return true; + } + } + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java index 57f2f44d7a..468173bdf3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java @@ -27,7 +27,7 @@ import lombok.NoArgsConstructor; public class TaskResult { private boolean success; - private boolean cancelled; + private boolean discarded; private TaskFailure failure; @Data diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java index ceba2645f9..8d69f5781b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -52,7 +52,7 @@ public class JobStatsService { } private void report(TenantId tenantId, JobId jobId, JobStatsMsg.Builder statsMsg) { - log.info("[{}] Reporting: {}", jobId, statsMsg); + log.debug("[{}] Reporting: {}", jobId, statsMsg); statsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setJobIdMSB(jobId.getId().getMostSignificantBits()) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 2cf0f22b90..f685373f75 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -57,7 +57,7 @@ public abstract class TaskProcessor { private ExecutorService consumerExecutor; private final Set deletedTenants = ConcurrentHashMap.newKeySet(); - private final Set cancelledJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine + private final Set discardedJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine @PostConstruct public void init() { @@ -83,14 +83,14 @@ public abstract class TaskProcessor { switch (entityId.getEntityType()) { case JOB -> { if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { - log.info("Adding job {} to cancelledJobs", entityId); - addToCancelledJobs(entityId.getId()); + log.debug("Adding job {} to discarded", entityId); + addToDiscardedJobs(entityId.getId()); } } case TENANT -> { if (event.getEvent() == ComponentLifecycleEvent.DELETED) { deletedTenants.add(entityId.getId()); - log.info("Adding tenant {} to deletedTenants", entityId); + log.debug("Adding tenant {} to deleted", entityId); } } } @@ -100,7 +100,7 @@ public abstract class TaskProcessor { for (TbProtoQueueMsg msg : msgs) { try { Task task = JacksonUtil.fromString(msg.getValue().getValue(), Task.class); - if (cancelledJobs.contains(task.getJobId().getId())) { + if (discardedJobs.contains(task.getJobId().getId())) { log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); reportCancelled(task); continue; @@ -157,13 +157,13 @@ public abstract class TaskProcessor { private void reportCancelled(Task task) { TaskResult result = TaskResult.builder() - .cancelled(true) + .discarded(true) .build(); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } - public void addToCancelledJobs(UUID jobId) { - cancelledJobs.add(jobId); + public void addToDiscardedJobs(UUID jobId) { + discardedJobs.add(jobId); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java similarity index 54% rename from dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java rename to dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 16b4397d87..c632a7b58b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.task; +package org.thingsboard.server.dao.job; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,17 +28,24 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import java.util.Optional; +import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED; +import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED; +import static org.thingsboard.server.common.data.job.JobStatus.FAILED; +import static org.thingsboard.server.common.data.job.JobStatus.PENDING; +import static org.thingsboard.server.common.data.job.JobStatus.QUEUED; +import static org.thingsboard.server.common.data.job.JobStatus.RUNNING; + @Service @RequiredArgsConstructor @Slf4j @@ -47,10 +54,16 @@ public class DefaultJobService extends AbstractEntityService implements JobServi private final JobDao jobDao; private final JobValidator validator = new JobValidator(); + @Transactional @Override public Job createJob(TenantId tenantId, Job job) { validator.validate(job, Job::getTenantId); - return saveJob(tenantId, job, false); + if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), PENDING, RUNNING)) { + job.setStatus(QUEUED); + } else { + job.setStatus(PENDING); + } + return saveJob(tenantId, job, true, null); } @Override @@ -62,11 +75,27 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public void cancelJob(TenantId tenantId, JobId jobId) { Job job = findForUpdate(tenantId, jobId); - if (job.getStatus() != JobStatus.PENDING && job.getStatus() != JobStatus.RUNNING) { + if (!job.getStatus().isOneOf(QUEUED, PENDING, RUNNING)) { throw new IllegalArgumentException("Job already " + job.getStatus().name().toLowerCase()); } job.getResult().setCancellationTs(System.currentTimeMillis()); - saveJob(tenantId, job, true); + JobStatus prevStatus = job.getStatus(); + if (job.getStatus() == QUEUED) { + job.setStatus(CANCELLED); // setting cancelled status right away, because we don't expect stats for cancelled tasks + } else if (job.getStatus() == PENDING) { + job.setStatus(RUNNING); + } + saveJob(tenantId, job, true, prevStatus); + } + + @Transactional + @Override + public void markAsFailed(TenantId tenantId, JobId jobId, String error) { + Job job = findForUpdate(tenantId, jobId); + job.getResult().setGeneralError(error); + JobStatus prevStatus = job.getStatus(); + job.setStatus(FAILED); + saveJob(tenantId, job, true, prevStatus); } @Transactional @@ -74,39 +103,36 @@ public class DefaultJobService extends AbstractEntityService implements JobServi public void processStats(TenantId tenantId, JobId jobId, JobStats jobStats) { Job job = findForUpdate(tenantId, jobId); if (job == null) { - log.info("Got stale stats for job {}: {}", jobId, jobStats); + log.debug("[{}][{}] Got stale stats: {}", tenantId, jobId, jobStats); return; } - switch (job.getStatus()) { - case PENDING -> { - job.setStatus(JobStatus.RUNNING); - } - case CANCELLED, COMPLETED, FAILED -> { - // got some stale stats - return; - } + JobStatus prevStatus = job.getStatus(); + if (job.getStatus() == PENDING) { + job.setStatus(RUNNING); } - JobResult jobResult = job.getResult(); + JobResult result = job.getResult(); if (jobStats.getTotalTasksCount() != null) { - jobResult.setTotalCount(jobStats.getTotalTasksCount()); + result.setTotalCount(jobStats.getTotalTasksCount()); } boolean publishEvent = false; for (TaskResult taskResult : jobStats.getTaskResults()) { if (taskResult.isSuccess()) { - jobResult.setSuccessfulCount(jobResult.getSuccessfulCount() + 1); - } else if (taskResult.isCancelled()) { - jobResult.setCancelledCount(jobResult.getCancelledCount() + 1); + result.setSuccessfulCount(result.getSuccessfulCount() + 1); + } else if (taskResult.isDiscarded()) { + result.setDiscardedCount(result.getDiscardedCount() + 1); } else { TaskFailure failure = taskResult.getFailure(); String key = failure.getTask().getKey(); - jobResult.setFailedCount(jobResult.getFailedCount() + 1); - jobResult.getFailures().put(key, failure.getError()); + result.setFailedCount(result.getFailedCount() + 1); + if (result.getFailures().size() < 1000) { // preserving only first 1000 errors, not reprocessing if there are more failures + result.getFailures().put(key, failure.getError()); + } } - if (jobResult.getCancellationTs() > 0) { - if (!taskResult.isCancelled() && System.currentTimeMillis() > jobResult.getCancellationTs()) { + if (result.getCancellationTs() > 0) { + if (!taskResult.isDiscarded() && System.currentTimeMillis() > result.getCancellationTs()) { log.info("Got task result for cancelled job {}: {}, re-notifying processors about cancellation", jobId, taskResult); // task processor forgot the task is cancelled publishEvent = true; @@ -114,32 +140,49 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } } - if (jobResult.getTotalCount() != null && jobResult.getCompletedCount() >= jobResult.getTotalCount()) { - if (jobResult.getCancellationTs() > 0) { - job.setStatus(JobStatus.CANCELLED); - } else if (jobResult.getFailedCount() > 0) { - job.setStatus(JobStatus.FAILED); - } else { - job.setStatus(JobStatus.COMPLETED); + if (job.getStatus() == RUNNING) { + if (result.getTotalCount() != null && result.getCompletedCount() >= result.getTotalCount()) { + if (result.getCancellationTs() > 0) { + job.setStatus(CANCELLED); + } else if (result.getFailedCount() > 0) { + job.setStatus(FAILED); + } else { + job.setStatus(COMPLETED); + } } } - log.info("Saving job {}", job); - saveJob(tenantId, job, publishEvent); + + saveJob(tenantId, job, publishEvent, prevStatus); } - private Job saveJob(TenantId tenantId, Job job, boolean publishEvent) { + private Job saveJob(TenantId tenantId, Job job, boolean publishEvent, JobStatus prevStatus) { job = jobDao.save(tenantId, job); if (publishEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(tenantId) .entityId(job.getId()) .entity(job) - .created(false) .build()); } + log.info("[{}] Saved job: {}", tenantId, job); + if (prevStatus != null && job.getStatus() != prevStatus) { + log.info("[{}][{}][{}] New job status: {} -> {}", tenantId, job.getId(), job.getType(), prevStatus, job.getStatus()); + if (job.getStatus().isOneOf(CANCELLED, COMPLETED, FAILED) && prevStatus != QUEUED) { // if prev status is QUEUED - means there are already running jobs with this type, no need to check for waiting job + checkWaitingJobs(tenantId, job.getType()); + } + } return job; } + private void checkWaitingJobs(TenantId tenantId, JobType jobType) { + Job queuedJob = jobDao.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId, jobType, QUEUED); + if (queuedJob == null) { + return; + } + queuedJob.setStatus(PENDING); + saveJob(tenantId, queuedJob, true, QUEUED); + } + @Override public PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink) { return jobDao.findByTenantId(tenantId, pageLink); @@ -149,15 +192,15 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return jobDao.findByIdForUpdate(tenantId, jobId); } - // todo: cancellation, reprocessing +// todo: reprocessing public class JobValidator extends DataValidator { @Override protected void validateCreate(TenantId tenantId, Job job) { - if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), JobStatus.PENDING, JobStatus.RUNNING)) { - throw new DataValidationException("Job of this type is already running"); - } +// if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), PENDING, RUNNING)) { +// throw new DataValidationException("Job of this type is already running"); +// } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java similarity index 90% rename from dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java rename to dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index 3b1d6631dd..799717fea8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/task/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.task; +package org.thingsboard.server.dao.job; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,4 +34,6 @@ public interface JobDao extends Dao { boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses); + Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java similarity index 56% rename from dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index 472df05bdd..bec5bf5f87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.task; +package org.thingsboard.server.dao.sql.job; import jakarta.persistence.LockModeType; -import jakarta.transaction.Transactional; +import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -36,7 +35,7 @@ import java.util.UUID; public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId " + - "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + + "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + "OR ilike(j.description, concat('%', :searchText, '%')) = true)") Page findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, @@ -46,45 +45,13 @@ public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.id = :id") JobEntity findByIdForUpdate(UUID id); - @Modifying - @Transactional - @Query(value = """ - UPDATE job - SET result = jsonb_set( - result, - '{successfulCount}', - to_jsonb((result->>'successfulCount')::int + :count) - ) - WHERE id = :jobId - RETURNING ((result->>'successfulCount')::int + :count) - + (result->>'failedCount')::int = (result->>'totalCount')::int - """, nativeQuery = true) - boolean reportTaskSuccess(@Param("jobId") UUID jobId, - @Param("count") int count); - - @Modifying - @Transactional - @Query(value = """ - UPDATE job - SET result = jsonb_set( - jsonb_set( - result, - '{failedCount}', - to_jsonb((result->>'failedCount')::int + 1) - ), - ARRAY['failures', :taskKey], - to_jsonb(:error) - ) - WHERE id = :jobId - RETURNING ((result->>'failedCount')::int + 1) + (result->>'successfulCount')::int - = (result->>'totalCount')::int - """, nativeQuery = true) - boolean reportTaskFailure(@Param("jobId") UUID jobId, - @Param("taskKey") String taskKey, - @Param("error") String error); - boolean existsByKeyAndStatusIn(String key, List statuses); boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); + @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE + @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.type = :type " + + "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") + JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, JobType type, JobStatus status, Limit limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java similarity index 87% rename from dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 92b9fc8a72..1b3a394e28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/task/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.task; +package org.thingsboard.server.dao.sql.job; import com.google.common.base.Strings; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; @@ -30,7 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.JobEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.task.JobDao; +import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Arrays; @@ -63,6 +64,11 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao return jobRepository.existsByTenantIdAndTypeAndStatusIn(tenantId.getId(), type, Arrays.stream(statuses).toList()); } + @Override + public Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status) { + return DaoUtil.getData(jobRepository.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId.getId(), type, status, Limit.of(1))); + } + @Override public EntityType getEntityType() { return EntityType.JOB; From 1562fc19d9440f1fd9d1538b9a22bfda93f4fb4f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 28 Apr 2025 17:33:52 +0300 Subject: [PATCH 07/44] Test CF reprocessing jobs --- .../server/controller/AbstractWebTest.java | 11 +++++++++++ .../server/service/job/JobManagerTest.java | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 33f2209eca..7e50168786 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -102,10 +102,12 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationType; @@ -128,6 +130,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; @@ -1253,4 +1256,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { new PageLink(limit, 0), unreadOnly, deliveryMethod).getData(); } + protected Job findJobById(JobId jobId) throws Exception { + return doGet("/api/job/" + jobId, Job.class); + } + + protected List findJobs() throws Exception { + return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 173a528e14..784b5bb2dd 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.job; -import com.fasterxml.jackson.core.type.TypeReference; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Before; @@ -31,9 +30,7 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -331,12 +328,4 @@ public class JobManagerTest extends AbstractControllerTest { // todo: job with zero tasks, reprocessing - private Job findJobById(JobId jobId) throws Exception { - return doGet("/api/job/" + jobId, Job.class); - } - - private List findJobs() throws Exception { - return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); - } - } \ No newline at end of file From 479ff8e25e1677b006a072eb12a645d052d54fc9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 30 Apr 2025 11:05:35 +0300 Subject: [PATCH 08/44] Jobs reprocessing --- .../server/controller/JobController.java | 7 ++ .../job/CfReprocessingJobProcessor.java | 28 ++++- .../server/service/job/DefaultJobManager.java | 69 ++++++++--- .../server/service/job/DummyJobProcessor.java | 36 ++++-- .../server/service/job/JobManager.java | 2 + .../server/service/job/JobProcessor.java | 4 + .../service/job/task/DummyTaskProcessor.java | 3 + .../server/controller/AbstractWebTest.java | 8 ++ .../server/service/job/JobManagerTest.java | 115 ++++++++++++++++-- .../server/dao/job/JobService.java | 2 +- .../job/CfReprocessingJobConfiguration.java | 8 +- .../common/data/job/CfReprocessingTask.java | 29 +++++ .../data/job/DummyJobConfiguration.java | 5 +- .../server/common/data/job/DummyTask.java | 32 +++++ .../common/data/job/JobConfiguration.java | 9 +- .../server/common/data/job/JobResult.java | 6 +- .../server/common/data/job/JobStatus.java | 1 + .../server/common/data/job/Task.java | 7 +- .../server/common/data/job/TaskFailure.java | 43 +++++++ .../server/common/data/job/TaskResult.java | 9 -- .../server/queue/task/TaskProcessor.java | 6 +- .../server/dao/job/DefaultJobService.java | 31 +---- 22 files changed, 373 insertions(+), 87 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index 5718d6e388..9b6627e12a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -82,4 +82,11 @@ public class JobController extends BaseController { jobManager.cancelJob(getTenantId(), new JobId(id)); } + @PostMapping("/job/{id}/reprocess") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void reprocessJob(@PathVariable UUID id) throws ThingsboardException { + // todo check permissions + jobManager.reprocessJob(getTenantId(), new JobId(id)); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java index edb38da5c2..79a735f6a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java @@ -24,19 +24,24 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.CfReprocessingJobConfiguration; import org.thingsboard.server.common.data.job.CfReprocessingTask; +import org.thingsboard.server.common.data.job.CfReprocessingTask.CfReprocessingTaskFailure; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskFailure; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; +import java.util.List; import java.util.function.Consumer; @Component @RequiredArgsConstructor public class CfReprocessingJobProcessor implements JobProcessor { + private final CalculatedFieldService calculatedFieldService; private final DeviceService deviceService; private final AssetService assetService; @@ -44,12 +49,12 @@ public class CfReprocessingJobProcessor implements JobProcessor { public int process(Job job, Consumer taskConsumer) throws Exception { CfReprocessingJobConfiguration configuration = job.getConfiguration(); - CalculatedField calculatedField = configuration.getCalculatedField(); + CalculatedField calculatedField = calculatedFieldService.findById(job.getTenantId(), configuration.getCalculatedFieldId()); EntityId cfEntityId = calculatedField.getEntityId(); int tasksCount = 0; if (cfEntityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { - taskConsumer.accept(createTask(job, configuration, cfEntityId)); + taskConsumer.accept(createTask(job, configuration, calculatedField, cfEntityId)); tasksCount++; } else { PageDataIterable entities; @@ -61,20 +66,31 @@ public class CfReprocessingJobProcessor implements JobProcessor { throw new IllegalArgumentException("Unsupported CF entity type " + cfEntityId.getEntityType()); } for (EntityId entityId : entities) { - taskConsumer.accept(createTask(job, configuration, entityId)); + taskConsumer.accept(createTask(job, configuration, calculatedField, entityId)); tasksCount++; } } return tasksCount; } - private Task createTask(Job job, CfReprocessingJobConfiguration configuration, EntityId entityId) { + @Override + public void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception { + CfReprocessingJobConfiguration configuration = job.getConfiguration(); + CalculatedField calculatedField = calculatedFieldService.findById(job.getTenantId(), configuration.getCalculatedFieldId()); + + for (TaskFailure failure : failures) { + CfReprocessingTaskFailure taskFailure = (CfReprocessingTaskFailure) failure; + EntityId entityId = taskFailure.getEntityId(); + taskConsumer.accept(createTask(job, job.getConfiguration(), calculatedField, entityId)); + } + } + + private Task createTask(Job job, CfReprocessingJobConfiguration configuration, CalculatedField calculatedField, EntityId entityId) { return CfReprocessingTask.builder() .tenantId(job.getTenantId()) .jobId(job.getId()) - .key(entityId.getEntityType().getNormalName() + " " + entityId.getId()) .retries(2) // 3 attempts in total - .calculatedField(configuration.getCalculatedField()) + .calculatedField(calculatedField) .entityId(entityId) .startTs(configuration.getStartTs()) .endTs(configuration.getEndTs()) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 6554bebe9f..b72974144d 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -27,10 +27,12 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskFailure; import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; @@ -48,6 +50,7 @@ import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -98,37 +101,73 @@ public class DefaultJobManager implements JobManager { @Override public Job submitJob(Job job) { log.debug("Submitting job: {}", job); - return jobService.createJob(job.getTenantId(), job); + return jobService.submitJob(job.getTenantId(), job); } @Override public void onJobUpdate(Job job) { if (job.getStatus() == JobStatus.PENDING) { executor.execute(() -> { - TenantId tenantId = job.getTenantId(); - JobId jobId = job.getId(); - try { - int tasksCount = jobProcessors.get(job.getType()).process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted - log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount); - jobStatsService.reportAllTasksSubmitted(tenantId, jobId, tasksCount); - } catch (Throwable e) { - log.error("[{}][{}][{}] Failed to submit tasks", tenantId, jobId, job.getType(), e); - try { - jobService.markAsFailed(tenantId, jobId, ExceptionUtils.getStackTrace(e)); - } catch (Throwable e2) { - log.error("[{}][{}] Failed to mark job as failed", tenantId, jobId, e2); - } - } + processJob(job); }); } } + private void processJob(Job job) { + TenantId tenantId = job.getTenantId(); + JobId jobId = job.getId(); + try { + JobProcessor processor = jobProcessors.get(job.getType()); + List toReprocess = job.getConfiguration().getToReprocess(); + if (toReprocess == null) { + int tasksCount = processor.process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted + log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount); + jobStatsService.reportAllTasksSubmitted(tenantId, jobId, tasksCount); + } else { + processor.reprocess(job, toReprocess, this::submitTask); + log.info("[{}][{}][{}] Submitted {} tasks for reprocessing", tenantId, jobId, job.getType(), toReprocess.size()); + } + } catch (Throwable e) { + log.error("[{}][{}][{}] Failed to submit tasks", tenantId, jobId, job.getType(), e); + try { + jobService.markAsFailed(tenantId, jobId, ExceptionUtils.getStackTrace(e)); + } catch (Throwable e2) { + log.error("[{}][{}] Failed to mark job as failed", tenantId, jobId, e2); + } + } + } + @Override public void cancelJob(TenantId tenantId, JobId jobId) { log.info("[{}][{}] Cancelling job", tenantId, jobId); jobService.cancelJob(tenantId, jobId); } + @Override + public void reprocessJob(TenantId tenantId, JobId jobId) { + log.info("[{}][{}] Reprocessing job", tenantId, jobId); + Job job = jobService.findJobById(tenantId, jobId); + if (job.getStatus() != JobStatus.FAILED) { + throw new IllegalArgumentException("Job is not failed"); + } + + JobResult result = job.getResult(); + if (result.getGeneralError() != null) { + throw new IllegalArgumentException("Reprocessing not allowed since job has general error"); + } + List failures = result.getFailures(); + if (result.getFailedCount() > failures.size()) { + throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + failures.size() + ")"); + } + + result.setFailedCount(0); + result.setFailures(Collections.emptyList()); + + job.getConfiguration().setToReprocess(failures); + + jobService.submitJob(tenantId, job); + } + private void submitTask(Task task) { log.info("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); TaskProto taskProto = TaskProto.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java index cda7201e1d..900b876c09 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -19,10 +19,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.job.DummyJobConfiguration; import org.thingsboard.server.common.data.job.DummyTask; +import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskFailure; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -35,31 +38,48 @@ public class DummyJobProcessor implements JobProcessor { DummyJobConfiguration configuration = job.getConfiguration(); if (configuration.getGeneralError() != null) { for (int number = 1; number <= configuration.getSubmittedTasksBeforeGeneralError(); number++) { - taskConsumer.accept(createTask(job, configuration, number, null)); + taskConsumer.accept(createTask(job, configuration, number, null, false)); } Thread.sleep(configuration.getTaskProcessingTimeMs() * (configuration.getSubmittedTasksBeforeGeneralError() / 2)); // sleeping so that some tasks are processed throw new RuntimeException(configuration.getGeneralError()); } - for (int number = 1; number <= configuration.getSuccessfulTasksCount(); number++) { - taskConsumer.accept(createTask(job, configuration, number, null)); + + int taskNumber = 1; + for (int i = 0; i < configuration.getSuccessfulTasksCount(); i++) { + taskConsumer.accept(createTask(job, configuration, taskNumber, null, false)); + taskNumber++; } if (configuration.getErrors() != null) { - for (int number = 1; number <= configuration.getFailedTasksCount(); number++) { - taskConsumer.accept(createTask(job, configuration, number, configuration.getErrors())); + for (int i = 0; i < configuration.getFailedTasksCount(); i++) { + taskConsumer.accept(createTask(job, configuration, taskNumber, configuration.getErrors(), false)); + taskNumber++; + } + for (int i = 0; i < configuration.getPermanentlyFailedTasksCount(); i++) { + taskConsumer.accept(createTask(job, configuration, taskNumber, configuration.getErrors(), true)); + taskNumber++; } } - return configuration.getSuccessfulTasksCount() + configuration.getFailedTasksCount(); + return configuration.getSuccessfulTasksCount() + configuration.getFailedTasksCount() + configuration.getPermanentlyFailedTasksCount(); + } + + @Override + public void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception { + for (TaskFailure failure : failures) { + DummyTaskFailure taskFailure = (DummyTaskFailure) failure; + taskConsumer.accept(createTask(job, job.getConfiguration(), taskFailure.getNumber(), taskFailure.isFailAlways() ? + List.of(taskFailure.getError()) : Collections.emptyList(), taskFailure.isFailAlways())); + } } - private Task createTask(Job job, DummyJobConfiguration configuration, int number, List errors) { + private Task createTask(Job job, DummyJobConfiguration configuration, int number, List errors, boolean failAlways) { return DummyTask.builder() .tenantId(job.getTenantId()) .jobId(job.getId()) - .key("Task " + number) .retries(configuration.getRetries()) .number(number) .processingTimeMs(configuration.getTaskProcessingTimeMs()) .errors(errors) + .failAlways(failAlways) .build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java index 3932361f3f..8e4858ebe3 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobManager.java @@ -25,6 +25,8 @@ public interface JobManager { void cancelJob(TenantId tenantId, JobId jobId); + void reprocessJob(TenantId tenantId, JobId jobId); + void onJobUpdate(Job job); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index 2431134e99..da2c75d166 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -18,13 +18,17 @@ package org.thingsboard.server.service.job; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; +import org.thingsboard.server.common.data.job.TaskFailure; +import java.util.List; import java.util.function.Consumer; public interface JobProcessor { int process(Job job, Consumer taskConsumer) throws Exception; + void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception; + JobType getType(); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index dc4a193bb9..73a27a0012 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -30,6 +30,9 @@ public class DummyTaskProcessor extends TaskProcessor { if (task.getProcessingTimeMs() > 0) { Thread.sleep(task.getProcessingTimeMs()); } + if (task.isFailAlways()) { + throw new RuntimeException(task.getErrors().get(0)); + } if (task.getErrors() != null && task.getAttempt() <= task.getErrors().size()) { String error = task.getErrors().get(task.getAttempt() - 1); throw new RuntimeException(error); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 7e50168786..d580a6b12a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1264,4 +1264,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } + protected void cancelJob(JobId jobId) throws Exception { + doPost("/api/job/" + jobId + "/cancel").andExpect(status().isOk()); + } + + protected void reprocessJob(JobId jobId) throws Exception { + doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 784b5bb2dd..f9881d0248 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -26,6 +26,7 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; +import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; @@ -130,8 +131,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks); - assertThat(jobResult.getFailures().get("Task 1")).isEqualTo("error3"); // last error - assertThat(jobResult.getFailures().get("Task 2")).isEqualTo("error3"); // last error + assertThat(jobResult.getFailures().get(0).getError()).isEqualTo("error3"); // last error + assertThat(jobResult.getFailures().get(1).getError()).isEqualTo("error3"); // last error assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); }); } @@ -151,7 +152,7 @@ public class JobManagerTest extends AbstractControllerTest { .build()).getId(); Thread.sleep(500); - jobManager.cancelJob(tenantId, jobId); + cancelJob(jobId); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); @@ -163,7 +164,7 @@ public class JobManagerTest extends AbstractControllerTest { } @Test - public void testCancelJob_simulateTaskProcessorRestart() { + public void testCancelJob_simulateTaskProcessorRestart() throws Exception { int tasksCount = 10; JobId jobId = jobManager.submitJob(Job.builder() .tenantId(tenantId) @@ -184,7 +185,7 @@ public class JobManagerTest extends AbstractControllerTest { } return null; }).when(taskProcessor).addToDiscardedJobs(any()); // ignoring cancellation event, - jobManager.cancelJob(tenantId, jobId); + cancelJob(jobId); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); @@ -262,7 +263,7 @@ public class JobManagerTest extends AbstractControllerTest { } @Test - public void testCancelQueuedJob() { + public void testCancelQueuedJob() throws Exception { int tasksCount = 3; int jobsCount = 3; List jobIds = new ArrayList<>(); @@ -281,7 +282,7 @@ public class JobManagerTest extends AbstractControllerTest { } for (int i = 1; i < jobIds.size(); i++) { - jobManager.cancelJob(tenantId, jobIds.get(i)); + cancelJob(jobIds.get(i)); } await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -326,6 +327,104 @@ public class JobManagerTest extends AbstractControllerTest { }); } - // todo: job with zero tasks, reprocessing + @Test + public void testJobReprocessing() throws Exception { + int successfulTasks = 3; + int failedTasks = 2; + int totalTasksCount = successfulTasks + failedTasks; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .errors(List.of("error")) + .taskProcessingTimeMs(100) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + JobResult jobResult = job.getResult(); + assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); + assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); + + for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { + DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + assertThat(failure.getNumber()).isEqualTo(taskNumber); + assertThat(failure.getError()).isEqualTo("error"); + } + }); + + reprocessJob(jobId); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(totalTasksCount); + assertThat(job.getResult().getFailedCount()).isZero(); + assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount); + assertThat(job.getResult().getFailures()).isEmpty(); + }); + } + + @Test + public void testJobReprocessing_somePermanentlyFailed() throws Exception { + int successfulTasks = 3; + int failedTasks = 2; + int permanentlyFailedTasks = 1; + int totalTasksCount = successfulTasks + failedTasks + permanentlyFailedTasks; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("test job") + .configuration(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .permanentlyFailedTasksCount(permanentlyFailedTasks) + .errors(List.of("error")) + .taskProcessingTimeMs(100) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + JobResult jobResult = job.getResult(); + assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); + assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks + permanentlyFailedTasks); + assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + + for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { + DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + assertThat(failure.getNumber()).isEqualTo(taskNumber); + assertThat(failure.getError()).isEqualTo("error"); + } + }); + + reprocessJob(jobId); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + JobResult jobResult = job.getResult(); + assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks + failedTasks); + assertThat(jobResult.getFailedCount()).isEqualTo(permanentlyFailedTasks); + assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + + for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { + DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + assertThat(failure.getNumber()).isEqualTo(taskNumber); + assertThat(failure.getError()).isEqualTo("error"); + assertThat(failure.isFailAlways()).isTrue(); + } + }); + } + + // todo: job with zero tasks } \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 62fd60eaac..dd333c072e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -25,7 +25,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService; public interface JobService extends EntityDaoService { - Job createJob(TenantId tenantId, Job job); + Job submitJob(TenantId tenantId, Job job); Job findJobById(TenantId tenantId, JobId jobId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java index 797dd0b639..90de047554 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java @@ -19,17 +19,19 @@ import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; @Data +@EqualsAndHashCode(callSuper = true) @AllArgsConstructor @NoArgsConstructor @Builder -public class CfReprocessingJobConfiguration implements JobConfiguration { +public class CfReprocessingJobConfiguration extends JobConfiguration { @NotNull - private CalculatedField calculatedField; + private CalculatedFieldId calculatedFieldId; private long startTs; private long endTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java index 8846a6a12f..3c8b765527 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java @@ -35,9 +35,38 @@ public class CfReprocessingTask extends Task { private long startTs; private long endTs; + @Override + public Object getKey() { + return entityId; + } + + @Override + public TaskFailure toFailure(Throwable error) { + return new CfReprocessingTaskFailure(entityId, error.getMessage()); + } + @Override public JobType getJobType() { return JobType.CF_REPROCESSING; } + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + public static class CfReprocessingTaskFailure extends TaskFailure { + + private EntityId entityId; + + public CfReprocessingTaskFailure(EntityId entityId, String error) { + super(error); + this.entityId = entityId; + } + + @Override + public JobType getJobType() { + return JobType.CF_REPROCESSING; + } + + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java index 70daf6f9c9..62695eb237 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -18,19 +18,22 @@ package org.thingsboard.server.common.data.job; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.List; @Data +@EqualsAndHashCode(callSuper = true) @AllArgsConstructor @NoArgsConstructor @Builder -public class DummyJobConfiguration implements JobConfiguration { +public class DummyJobConfiguration extends JobConfiguration { private long taskProcessingTimeMs; private int successfulTasksCount; private int failedTasksCount; + private int permanentlyFailedTasksCount; private List errors; private int retries; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java index ae93ed06bb..ac15dc63cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java @@ -33,10 +33,42 @@ public class DummyTask extends Task { private int number; private long processingTimeMs; private List errors; // errors for each attempt + private boolean failAlways; + + @Override + public Object getKey() { + return number; + } + + @Override + public TaskFailure toFailure(Throwable error) { + return new DummyTaskFailure(number, failAlways, error.getMessage()); + } @Override public JobType getJobType() { return JobType.DUMMY; } + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + public static class DummyTaskFailure extends TaskFailure { + + private int number; + private boolean failAlways; + + public DummyTaskFailure(int number, boolean failAlways, String error) { + super(error); + this.number = number; + this.failAlways = failAlways; + } + + @Override + public JobType getJobType() { + return JobType.DUMMY; + } + + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index b541458289..0d3620d9e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -19,8 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; import java.io.Serializable; +import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @@ -28,8 +30,11 @@ import java.io.Serializable; @Type(name = "CF_REPROCESSING", value = CfReprocessingJobConfiguration.class), @Type(name = "DUMMY", value = DummyJobConfiguration.class), }) -public interface JobConfiguration extends Serializable { +@Data +public abstract class JobConfiguration implements Serializable { - JobType getType(); + private List toReprocess; + + public abstract JobType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index b517877906..748d24811c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -24,8 +24,8 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @@ -41,7 +41,7 @@ public abstract class JobResult implements Serializable { private int failedCount; private int discardedCount; private Integer totalCount = null; // set when all tasks are submitted - private Map failures = new HashMap<>(); + private List failures = new ArrayList<>(); private String generalError; private long cancellationTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java index 17d050a540..5a4a9e35ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStatus.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.job; public enum JobStatus { + QUEUED, PENDING, RUNNING, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java index 5ef735f5d3..6399f6f79a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -38,7 +39,6 @@ public abstract class Task { private TenantId tenantId; private JobId jobId; - private String key; private int retries; public Task() { @@ -46,6 +46,11 @@ public abstract class Task { private int attempt = 0; + @JsonIgnore + public abstract Object getKey(); + + public abstract TaskFailure toFailure(Throwable error); + public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java new file mode 100644 index 0000000000..1e365c6a8f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java @@ -0,0 +1,43 @@ +/** + * 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.common.data.job; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.job.CfReprocessingTask.CfReprocessingTaskFailure; +import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") +@JsonSubTypes({ + @Type(name = "CF_REPROCESSING", value = CfReprocessingTaskFailure.class), + @Type(name = "DUMMY", value = DummyTaskFailure.class) +}) +public abstract class TaskFailure { + + private String error; + + public abstract JobType getJobType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java index 468173bdf3..0ee9a2b477 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java @@ -30,13 +30,4 @@ public class TaskResult { private boolean discarded; private TaskFailure failure; - @Data - @AllArgsConstructor - @NoArgsConstructor - @Builder - public static class TaskFailure { - private String error; - private Task task; - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index f685373f75..f0ba542ab9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.Task; import org.thingsboard.server.common.data.job.TaskResult; -import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; @@ -147,10 +146,7 @@ public abstract class TaskProcessor { private void reportFailure(Task task, Throwable error) { TaskResult result = TaskResult.builder() - .failure(TaskFailure.builder() - .error(error.getMessage()) - .task(task) - .build()) + .failure(task.toFailure(error)) .build(); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index c632a7b58b..f2802f4771 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -30,12 +30,11 @@ import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.TaskResult; -import org.thingsboard.server.common.data.job.TaskResult.TaskFailure; +import org.thingsboard.server.common.data.job.TaskFailure; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.service.DataValidator; import java.util.Optional; @@ -52,12 +51,13 @@ import static org.thingsboard.server.common.data.job.JobStatus.RUNNING; public class DefaultJobService extends AbstractEntityService implements JobService { private final JobDao jobDao; - private final JobValidator validator = new JobValidator(); @Transactional @Override - public Job createJob(TenantId tenantId, Job job) { - validator.validate(job, Job::getTenantId); + public Job submitJob(TenantId tenantId, Job job) { + if (jobDao.existsByKeyAndStatusOneOf(job.getKey(), QUEUED, PENDING, RUNNING)) { + throw new IllegalArgumentException("The same job is already queued or running"); + } if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), PENDING, RUNNING)) { job.setStatus(QUEUED); } else { @@ -124,10 +124,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi result.setDiscardedCount(result.getDiscardedCount() + 1); } else { TaskFailure failure = taskResult.getFailure(); - String key = failure.getTask().getKey(); result.setFailedCount(result.getFailedCount() + 1); if (result.getFailures().size() < 1000) { // preserving only first 1000 errors, not reprocessing if there are more failures - result.getFailures().put(key, failure.getError()); + result.getFailures().add(failure); } } @@ -192,24 +191,6 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return jobDao.findByIdForUpdate(tenantId, jobId); } -// todo: reprocessing - - public class JobValidator extends DataValidator { - - @Override - protected void validateCreate(TenantId tenantId, Job job) { -// if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), PENDING, RUNNING)) { -// throw new DataValidationException("Job of this type is already running"); -// } - } - - @Override - protected Job validateUpdate(TenantId tenantId, Job job) { - throw new IllegalArgumentException("Job can't be updated externally"); - } - - } - @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findJobById(tenantId, (JobId) entityId)); From 44a4d9d69040b6f9ad36d1591491489cab491a7e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 30 Apr 2025 11:25:32 +0300 Subject: [PATCH 09/44] Cleanup code --- .../server/actors/ActorSystemContext.java | 6 + .../actors/ruleChain/DefaultTbContext.java | 8 +- .../job/CfReprocessingJobProcessor.java | 105 ------------------ .../job/task/CfReprocessingTaskProcessor.java | 57 ---------- .../job/CfReprocessingJobConfiguration.java | 43 ------- .../data/job/CfReprocessingJobResult.java | 25 ----- .../common/data/job/CfReprocessingTask.java | 72 ------------ .../server/common/data/job/Job.java | 1 - .../common/data/job/JobConfiguration.java | 1 - .../server/common/data/job/JobResult.java | 1 - .../server/common/data/job/JobType.java | 1 - .../server/common/data/job/Task.java | 1 - .../server/common/data/job/TaskFailure.java | 2 - .../main/resources/sql/schema-entities.sql | 2 +- .../rule/engine/api/TbContext.java | 3 + .../rule/engine/util/TenantIdLoader.java | 4 + .../rule/engine/util/TenantIdLoaderTest.java | 10 ++ 17 files changed, 31 insertions(+), 311 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.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 26c82a33de..b2845085fd 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -95,6 +95,7 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeStateService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -551,6 +552,11 @@ public class ActorSystemContext { @Getter private CalculatedFieldQueueService calculatedFieldQueueService; + @Lazy + @Autowired(required = false) + @Getter + private JobService jobService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private int maxConcurrentSessionsPerDevice; 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 033e10ca9a..e40453da25 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 @@ -23,6 +23,7 @@ import org.bouncycastle.util.Arrays; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; @@ -30,7 +31,6 @@ import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; -import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -107,6 +107,7 @@ import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; @@ -887,6 +888,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getCalculatedFieldQueueService(); } + @Override + public JobService getJobService() { + return mainCtx.getJobService(); + } + @Override public boolean isExternalNodeForceAck() { return mainCtx.isExternalNodeForceAck(); diff --git a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java deleted file mode 100644 index 79a735f6a6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/job/CfReprocessingJobProcessor.java +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 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.job; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.job.CfReprocessingJobConfiguration; -import org.thingsboard.server.common.data.job.CfReprocessingTask; -import org.thingsboard.server.common.data.job.CfReprocessingTask.CfReprocessingTaskFailure; -import org.thingsboard.server.common.data.job.Job; -import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.Task; -import org.thingsboard.server.common.data.job.TaskFailure; -import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.device.DeviceService; - -import java.util.List; -import java.util.function.Consumer; - -@Component -@RequiredArgsConstructor -public class CfReprocessingJobProcessor implements JobProcessor { - - private final CalculatedFieldService calculatedFieldService; - private final DeviceService deviceService; - private final AssetService assetService; - - @Override - public int process(Job job, Consumer taskConsumer) throws Exception { - CfReprocessingJobConfiguration configuration = job.getConfiguration(); - - CalculatedField calculatedField = calculatedFieldService.findById(job.getTenantId(), configuration.getCalculatedFieldId()); - EntityId cfEntityId = calculatedField.getEntityId(); - - int tasksCount = 0; - if (cfEntityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { - taskConsumer.accept(createTask(job, configuration, calculatedField, cfEntityId)); - tasksCount++; - } else { - PageDataIterable entities; - if (cfEntityId.getEntityType() == EntityType.DEVICE_PROFILE) { - entities = new PageDataIterable<>(pageLink -> deviceService.findDeviceIdsByTenantIdAndDeviceProfileId(job.getTenantId(), (DeviceProfileId) cfEntityId, pageLink), 512); - } else if (cfEntityId.getEntityType() == EntityType.ASSET_PROFILE) { - entities = new PageDataIterable<>(pageLink -> assetService.findAssetIdsByTenantIdAndAssetProfileId(job.getTenantId(), (AssetProfileId) cfEntityId, pageLink), 512); - } else { - throw new IllegalArgumentException("Unsupported CF entity type " + cfEntityId.getEntityType()); - } - for (EntityId entityId : entities) { - taskConsumer.accept(createTask(job, configuration, calculatedField, entityId)); - tasksCount++; - } - } - return tasksCount; - } - - @Override - public void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception { - CfReprocessingJobConfiguration configuration = job.getConfiguration(); - CalculatedField calculatedField = calculatedFieldService.findById(job.getTenantId(), configuration.getCalculatedFieldId()); - - for (TaskFailure failure : failures) { - CfReprocessingTaskFailure taskFailure = (CfReprocessingTaskFailure) failure; - EntityId entityId = taskFailure.getEntityId(); - taskConsumer.accept(createTask(job, job.getConfiguration(), calculatedField, entityId)); - } - } - - private Task createTask(Job job, CfReprocessingJobConfiguration configuration, CalculatedField calculatedField, EntityId entityId) { - return CfReprocessingTask.builder() - .tenantId(job.getTenantId()) - .jobId(job.getId()) - .retries(2) // 3 attempts in total - .calculatedField(calculatedField) - .entityId(entityId) - .startTs(configuration.getStartTs()) - .endTs(configuration.getEndTs()) - .build(); - } - - @Override - public JobType getType() { - return JobType.CF_REPROCESSING; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java deleted file mode 100644 index 5d4005307c..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/job/task/CfReprocessingTaskProcessor.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 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.job.task; - -import com.google.common.util.concurrent.SettableFuture; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.thingsboard.server.actors.calculatedField.CalculatedFieldReprocessingService; -import org.thingsboard.server.common.data.job.CfReprocessingTask; -import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.queue.task.TaskProcessor; - -import java.util.concurrent.TimeUnit; - -@Component -@RequiredArgsConstructor -public class CfReprocessingTaskProcessor extends TaskProcessor { - - private final CalculatedFieldReprocessingService cfReprocessingService; - - @Override - public void process(CfReprocessingTask task) throws Exception { - SettableFuture future = SettableFuture.create(); - cfReprocessingService.reprocess(task, new TbCallback() { - @Override - public void onSuccess() { - future.set(null); - } - - @Override - public void onFailure(Throwable t) { - future.setException(t); - } - }); - future.get(1, TimeUnit.MINUTES); - } - - @Override - public JobType getJobType() { - return JobType.CF_REPROCESSING; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java deleted file mode 100644 index 90de047554..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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.common.data.job; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.id.CalculatedFieldId; - -@Data -@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class CfReprocessingJobConfiguration extends JobConfiguration { - - @NotNull - private CalculatedFieldId calculatedFieldId; - private long startTs; - private long endTs; - - @Override - public JobType getType() { - return JobType.CF_REPROCESSING; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java deleted file mode 100644 index 2d756f6d53..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingJobResult.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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.common.data.job; - -public class CfReprocessingJobResult extends JobResult { - - @Override - public JobType getJobType() { - return JobType.CF_REPROCESSING; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java deleted file mode 100644 index 3c8b765527..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/CfReprocessingTask.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * 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.common.data.job; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import lombok.experimental.SuperBuilder; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.EntityId; - -@Data -@NoArgsConstructor -@EqualsAndHashCode(callSuper = true) -@SuperBuilder -@ToString(callSuper = true) -public class CfReprocessingTask extends Task { - - private CalculatedField calculatedField; - private EntityId entityId; - private long startTs; - private long endTs; - - @Override - public Object getKey() { - return entityId; - } - - @Override - public TaskFailure toFailure(Throwable error) { - return new CfReprocessingTaskFailure(entityId, error.getMessage()); - } - - @Override - public JobType getJobType() { - return JobType.CF_REPROCESSING; - } - - @Data - @EqualsAndHashCode(callSuper = true) - @NoArgsConstructor - public static class CfReprocessingTaskFailure extends TaskFailure { - - private EntityId entityId; - - public CfReprocessingTaskFailure(EntityId entityId, String error) { - super(error); - this.entityId = entityId; - } - - @Override - public JobType getJobType() { - return JobType.CF_REPROCESSING; - } - - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index e96d42cad1..237c223a92 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -54,7 +54,6 @@ public class Job extends BaseData implements HasTenantId { this.description = description; this.configuration = configuration; this.result = switch (type) { - case CF_REPROCESSING -> new CfReprocessingJobResult(); case DUMMY -> new DummyJobResult(); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 0d3620d9e8..7a2eccd42a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -27,7 +27,6 @@ import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ - @Type(name = "CF_REPROCESSING", value = CfReprocessingJobConfiguration.class), @Type(name = "DUMMY", value = DummyJobConfiguration.class), }) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 748d24811c..bf5e5f2c56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -30,7 +30,6 @@ import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @Type(name = "CF_REPROCESSING", value = CfReprocessingJobResult.class), @Type(name = "DUMMY", value = DummyJobResult.class) }) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java index 7c0d9972e1..9e8e9fa7e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.job; public enum JobType { - CF_REPROCESSING, DUMMY; public String getTasksTopic() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java index 6399f6f79a..ad7c6b62df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.id.TenantId; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @Type(name = "CF_REPROCESSING", value = CfReprocessingTask.class), @Type(name = "DUMMY", value = DummyTask.class) }) @SuperBuilder diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java index 1e365c6a8f..7a04db8188 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.job.CfReprocessingTask.CfReprocessingTaskFailure; import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; @Data @@ -31,7 +30,6 @@ import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @Type(name = "CF_REPROCESSING", value = CfReprocessingTaskFailure.class), @Type(name = "DUMMY", value = DummyTaskFailure.class) }) public abstract class TaskFailure { diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5afc9398d2..ba43a54697 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -957,6 +957,6 @@ CREATE TABLE IF NOT EXISTS job ( key varchar NOT NULL, description varchar NOT NULL, status varchar NOT NULL, - configuration varchar(1000) NOT NULL, + configuration varchar(1000000) NOT NULL, result jsonb ); 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 b66c9e13d5..d4d19dd653 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 @@ -77,6 +77,7 @@ import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; @@ -362,6 +363,8 @@ public interface TbContext { RuleEngineCalculatedFieldQueueService getCalculatedFieldQueueService(); + JobService getJobService(); + boolean isExternalNodeForceAck(); /** diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index f12a856567..93fad4c0e7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.NotificationRequestId; @@ -175,6 +176,9 @@ public class TenantIdLoader { tenantEntity = null; } break; + case JOB: + tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 38417c3922..4cbc091bdc 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -88,6 +89,7 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -160,6 +162,8 @@ public class TenantIdLoaderTest { private MobileAppBundleService mobileAppBundleService; @Mock private CalculatedFieldService calculatedFieldService; + @Mock + private JobService jobService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -419,6 +423,12 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); break; + case JOB: + Job job = new Job(); + job.setTenantId(tenantId); + when(ctx.getJobService()).thenReturn(jobService); + doReturn(job).when(jobService).findJobById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } From 5f9afd14d383c3451cea5001edcbb119530ee4a4 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 May 2025 08:59:58 +0300 Subject: [PATCH 10/44] added findByKey method to service --- .../java/org/thingsboard/server/dao/job/JobService.java | 2 ++ .../org/thingsboard/server/dao/job/DefaultJobService.java | 7 ++++++- .../main/java/org/thingsboard/server/dao/job/JobDao.java | 2 ++ .../org/thingsboard/server/dao/sql/job/JobRepository.java | 6 ++++-- .../java/org/thingsboard/server/dao/sql/job/JpaJobDao.java | 7 ++++++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index dd333c072e..65b21853b8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -37,4 +37,6 @@ public interface JobService extends EntityDaoService { PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); + Job findJobByKey(TenantId tenantId, String key); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index f2802f4771..6cc315c8f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -29,8 +29,8 @@ import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.job.TaskFailure; +import org.thingsboard.server.common.data.job.TaskResult; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -187,6 +187,11 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return jobDao.findByTenantId(tenantId, pageLink); } + @Override + public Job findJobByKey(TenantId tenantId, String key) { + return jobDao.findByKey(tenantId, key); + } + private Job findForUpdate(TenantId tenantId, JobId jobId) { return jobDao.findByIdForUpdate(tenantId, jobId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index 799717fea8..a892fb3e22 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -30,6 +30,8 @@ public interface JobDao extends Dao { Job findByIdForUpdate(TenantId tenantId, JobId jobId); + Job findByKey(TenantId tenantId, String key); + boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses); boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index bec5bf5f87..bbbdbf3a47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -35,8 +35,8 @@ import java.util.UUID; public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId " + - "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + - "OR ilike(j.description, concat('%', :searchText, '%')) = true)") + "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + + "OR ilike(j.description, concat('%', :searchText, '%')) = true)") Page findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @@ -45,6 +45,8 @@ public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.id = :id") JobEntity findByIdForUpdate(UUID id); + JobEntity findByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key); + boolean existsByKeyAndStatusIn(String key, List statuses); boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 1b3a394e28..71bbdb2f79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -29,9 +29,9 @@ import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.model.sql.JobEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Arrays; @@ -54,6 +54,11 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao return DaoUtil.getData(jobRepository.findByIdForUpdate(jobId.getId())); } + @Override + public Job findByKey(TenantId tenantId, String key) { + return DaoUtil.getData(jobRepository.findByTenantIdAndKey(tenantId.getId(), key)); + } + @Override public boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses) { return jobRepository.existsByKeyAndStatusIn(key, Arrays.stream(statuses).toList()); From 874d59a706014af8d3e6a2acb1aea69c927f1eb1 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 May 2025 11:59:41 +0300 Subject: [PATCH 11/44] updated methods --- .../java/org/thingsboard/server/dao/job/JobService.java | 2 +- .../org/thingsboard/server/dao/job/DefaultJobService.java | 6 +++--- .../main/java/org/thingsboard/server/dao/job/JobDao.java | 4 ++-- .../org/thingsboard/server/dao/sql/job/JobRepository.java | 6 +++--- .../org/thingsboard/server/dao/sql/job/JpaJobDao.java | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 65b21853b8..3c00b2fe0e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -37,6 +37,6 @@ public interface JobService extends EntityDaoService { PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); - Job findJobByKey(TenantId tenantId, String key); + Job findLatestJobByKey(TenantId tenantId, String key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 6cc315c8f8..ce4aa47741 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -55,7 +55,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Transactional @Override public Job submitJob(TenantId tenantId, Job job) { - if (jobDao.existsByKeyAndStatusOneOf(job.getKey(), QUEUED, PENDING, RUNNING)) { + if (jobDao.existsByTenantAndKeyAndStatusOneOf(tenantId, job.getKey(), QUEUED, PENDING, RUNNING)) { throw new IllegalArgumentException("The same job is already queued or running"); } if (jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, job.getType(), PENDING, RUNNING)) { @@ -188,8 +188,8 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } @Override - public Job findJobByKey(TenantId tenantId, String key) { - return jobDao.findByKey(tenantId, key); + public Job findLatestJobByKey(TenantId tenantId, String key) { + return jobDao.findLatestByKey(tenantId, key); } private Job findForUpdate(TenantId tenantId, JobId jobId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index a892fb3e22..67a234245d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -30,9 +30,9 @@ public interface JobDao extends Dao { Job findByIdForUpdate(TenantId tenantId, JobId jobId); - Job findByKey(TenantId tenantId, String key); + Job findLatestByKey(TenantId tenantId, String key); - boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses); + boolean existsByTenantAndKeyAndStatusOneOf(TenantId tenantId, String key, JobStatus... statuses); boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index bbbdbf3a47..0cc43375fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -45,15 +45,15 @@ public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.id = :id") JobEntity findByIdForUpdate(UUID id); - JobEntity findByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key); + JobEntity findLatestByTenantIdAndKey(UUID tenantId, String key); - boolean existsByKeyAndStatusIn(String key, List statuses); + boolean existsByTenantIdAndKeyAndStatusIn(UUID tenantId, String key, List statuses); boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.type = :type " + - "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") + "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, JobType type, JobStatus status, Limit limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 71bbdb2f79..7d8def5792 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -55,13 +55,13 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao } @Override - public Job findByKey(TenantId tenantId, String key) { - return DaoUtil.getData(jobRepository.findByTenantIdAndKey(tenantId.getId(), key)); + public Job findLatestByKey(TenantId tenantId, String key) { + return DaoUtil.getData(jobRepository.findLatestByTenantIdAndKey(tenantId.getId(), key)); } @Override - public boolean existsByKeyAndStatusOneOf(String key, JobStatus... statuses) { - return jobRepository.existsByKeyAndStatusIn(key, Arrays.stream(statuses).toList()); + public boolean existsByTenantAndKeyAndStatusOneOf(TenantId tenantId, String key, JobStatus... statuses) { + return jobRepository.existsByTenantIdAndKeyAndStatusIn(tenantId.getId(), key, Arrays.stream(statuses).toList()); } @Override From d357be9209e7ef6826114bc6a839d7a67135ebd6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 1 May 2025 12:18:10 +0300 Subject: [PATCH 12/44] Minor refactoring --- .../service/job/task/DummyTaskProcessor.java | 5 +- .../server/queue/task/TaskProcessor.java | 4 +- .../thingsboard/rest/client/RestClient.java | 49 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 73a27a0012..361dbfbde1 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -23,10 +23,10 @@ import org.thingsboard.server.queue.task.TaskProcessor; @Component @RequiredArgsConstructor -public class DummyTaskProcessor extends TaskProcessor { +public class DummyTaskProcessor extends TaskProcessor { @Override - public void process(DummyTask task) throws Exception { + public Void process(DummyTask task) throws Exception { if (task.getProcessingTimeMs() > 0) { Thread.sleep(task.getProcessingTimeMs()); } @@ -37,6 +37,7 @@ public class DummyTaskProcessor extends TaskProcessor { String error = task.getErrors().get(task.getAttempt() - 1); throw new RuntimeException(error); } + return null; } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index f0ba542ab9..57dd46a6e5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -43,7 +43,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public abstract class TaskProcessor { +public abstract class TaskProcessor { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -135,7 +135,7 @@ public abstract class TaskProcessor { } } - public abstract void process(T task) throws Exception; + public abstract R process(T task) throws Exception; private void reportSuccess(Task task) { TaskResult result = TaskResult.builder() diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 4f7538d205..eb051b19e5 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -19,7 +19,9 @@ import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; +import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.concurrent.LazyInitializer; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -55,9 +57,9 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; @@ -205,7 +207,9 @@ public class RestClient implements Closeable { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; private static final long AVG_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(30); protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; - private final ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass()); + private final LazyInitializer executor = LazyInitializer.builder() + .setInitializer(() -> ThingsBoardExecutors.newWorkStealingPool(10, getClass())) + .get(); protected final RestTemplate restTemplate; protected final RestTemplate loginRestTemplate; protected final String baseURL; @@ -223,22 +227,30 @@ public class RestClient implements Closeable { } public RestClient(RestTemplate restTemplate, String baseURL) { + this(restTemplate, baseURL, null); + } + + public RestClient(RestTemplate restTemplate, String baseURL, String accessToken) { this.restTemplate = restTemplate; this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory()); this.baseURL = baseURL; this.restTemplate.getInterceptors().add((request, bytes, execution) -> { HttpRequest wrapper = new HttpRequestWrapper(request); - long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT; - if (calculatedTs > mainTokenExpTs) { - synchronized (RestClient.this) { - if (calculatedTs > mainTokenExpTs) { - if (calculatedTs < refreshTokenExpTs) { - refreshToken(); - } else { - doLogin(); + if (accessToken == null) { + long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT; + if (calculatedTs > mainTokenExpTs) { + synchronized (RestClient.this) { + if (calculatedTs > mainTokenExpTs) { + if (calculatedTs < refreshTokenExpTs) { + refreshToken(); + } else { + doLogin(); + } } } } + } else { + mainToken = accessToken; } wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + mainToken); return execution.execute(wrapper, bytes); @@ -2403,7 +2415,7 @@ public class RestClient implements Closeable { } public Future> getAttributeKvEntriesAsync(EntityId entityId, List keys) { - return service.submit(() -> getAttributeKvEntries(entityId, keys)); + return getExecutor().submit(() -> getAttributeKvEntries(entityId, keys)); } public List getAttributesByScope(EntityId entityId, String scope, List keys) { @@ -2976,7 +2988,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypes?" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -3064,7 +3076,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypesInfos?widgetsBundleId={widgetsBundleId}&" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -3765,7 +3777,7 @@ public class RestClient implements Closeable { } public PageData getImages(PageLink pageLink, boolean includeSystemImages) { - return this.getImages(pageLink, null, includeSystemImages); + return this.getImages(pageLink, null, includeSystemImages); } public PageData getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) { @@ -4175,7 +4187,14 @@ public class RestClient implements Closeable { @Override public void close() { - service.shutdown(); + if (executor.isInitialized()) { + getExecutor().shutdown(); + } + } + + @SneakyThrows + private ExecutorService getExecutor() { + return executor.get(); } } From 97b45a6bf917a00b7e0eba98a88a9522903af8a6 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 May 2025 12:23:59 +0300 Subject: [PATCH 13/44] fixed tests --- .../org/thingsboard/server/dao/job/DefaultJobService.java | 5 +++++ .../main/java/org/thingsboard/server/dao/job/JobDao.java | 2 ++ .../org/thingsboard/server/dao/sql/job/JobRepository.java | 6 +++++- .../java/org/thingsboard/server/dao/sql/job/JpaJobDao.java | 5 +++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index ce4aa47741..3e511ccfe4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -206,6 +206,11 @@ public class DefaultJobService extends AbstractEntityService implements JobServi jobDao.removeById(tenantId, id.getId()); } + @Override + public void deleteByTenantId(TenantId tenantId) { + jobDao.deleteByTenantId(tenantId); + } + @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index 67a234245d..afe182d8cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -38,4 +38,6 @@ public interface JobDao extends Dao { Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status); + void deleteByTenantId(TenantId tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index 0cc43375fe..a4d280aba4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -45,7 +45,9 @@ public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.id = :id") JobEntity findByIdForUpdate(UUID id); - JobEntity findLatestByTenantIdAndKey(UUID tenantId, String key); + @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.key = :key " + + "ORDER BY j.createdTime DESC") + JobEntity findLatestByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key); boolean existsByTenantIdAndKeyAndStatusIn(UUID tenantId, String key, List statuses); @@ -56,4 +58,6 @@ public interface JobRepository extends JpaRepository { "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, JobType type, JobStatus status, Limit limit); + void deleteByTenantId(UUID tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 7d8def5792..0e5ee4683e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -74,6 +74,11 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao return DaoUtil.getData(jobRepository.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId.getId(), type, status, Limit.of(1))); } + @Override + public void deleteByTenantId(TenantId tenantId) { + jobRepository.deleteByTenantId(tenantId.getId()); + } + @Override public EntityType getEntityType() { return EntityType.JOB; From 762abebf790f4255e790dca886baa3f8e047740c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 May 2025 12:33:12 +0300 Subject: [PATCH 14/44] added query --- .../org/thingsboard/server/dao/sql/job/JobRepository.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index a4d280aba4..0ecd517f51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -21,9 +21,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.dao.model.sql.JobEntity; @@ -58,6 +60,9 @@ public interface JobRepository extends JpaRepository { "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, JobType type, JobStatus status, Limit limit); + @Transactional + @Modifying + @Query("DELETE FROM JobEntity j WHERE j.tenantId = :tenantId") void deleteByTenantId(UUID tenantId); } From 9e878923e1297386c0800f6488bddc6a5cb3a132 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 1 May 2025 13:41:36 +0300 Subject: [PATCH 15/44] Refactor job and task results --- .../server/service/job/DefaultJobManager.java | 24 +++++----- .../server/service/job/DummyJobProcessor.java | 23 ++++----- .../server/service/job/JobProcessor.java | 8 ++-- .../service/job/task/DummyTaskProcessor.java | 9 ++-- .../server/service/job/JobManagerTest.java | 17 +++---- .../common/data/job/JobConfiguration.java | 3 +- .../server/common/data/job/JobResult.java | 16 ++++++- .../server/common/data/job/JobStats.java | 1 + .../common/data/job/{ => task}/DummyTask.java | 35 +++++++------- .../common/data/job/task/DummyTaskResult.java | 48 +++++++++++++++++++ .../common/data/job/{ => task}/Task.java | 9 ++-- .../TaskFailure.java} | 12 ++--- .../TaskResult.java} | 17 +++++-- .../server/queue/task/JobStatsService.java | 2 +- .../server/queue/task/TaskProcessor.java | 39 +++++++-------- .../server/dao/job/DefaultJobService.java | 15 +----- 16 files changed, 169 insertions(+), 109 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/job/{ => task}/DummyTask.java (63%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java rename common/data/src/main/java/org/thingsboard/server/common/data/job/{ => task}/Task.java (85%) rename common/data/src/main/java/org/thingsboard/server/common/data/job/{TaskResult.java => task/TaskFailure.java} (79%) rename common/data/src/main/java/org/thingsboard/server/common/data/job/{TaskFailure.java => task/TaskResult.java} (74%) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index b72974144d..3d601a9358 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -31,9 +31,8 @@ import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.Task; -import org.thingsboard.server.common.data.job.TaskFailure; -import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.task.Task; +import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; @@ -50,7 +49,6 @@ import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,7 +116,7 @@ public class DefaultJobManager implements JobManager { JobId jobId = job.getId(); try { JobProcessor processor = jobProcessors.get(job.getType()); - List toReprocess = job.getConfiguration().getToReprocess(); + List toReprocess = job.getConfiguration().getToReprocess(); if (toReprocess == null) { int tasksCount = processor.process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount); @@ -155,20 +153,24 @@ public class DefaultJobManager implements JobManager { if (result.getGeneralError() != null) { throw new IllegalArgumentException("Reprocessing not allowed since job has general error"); } - List failures = result.getFailures(); - if (result.getFailedCount() > failures.size()) { - throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + failures.size() + ")"); + List taskFailures = result.getResults().stream() + .filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded()) + .toList(); + if (result.getFailedCount() > taskFailures.size()) { + throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")"); } result.setFailedCount(0); - result.setFailures(Collections.emptyList()); + result.setResults(result.getResults().stream() + .filter(TaskResult::isSuccess) + .toList()); - job.getConfiguration().setToReprocess(failures); + job.getConfiguration().setToReprocess(taskFailures); jobService.submitJob(tenantId, job); } - private void submitTask(Task task) { + private void submitTask(Task task) { log.info("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); TaskProto taskProto = TaskProto.newBuilder() .setValue(JacksonUtil.toString(task)) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java index 900b876c09..64148af259 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -18,12 +18,13 @@ package org.thingsboard.server.service.job; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.job.DummyJobConfiguration; -import org.thingsboard.server.common.data.job.DummyTask; -import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.Task; -import org.thingsboard.server.common.data.job.TaskFailure; +import org.thingsboard.server.common.data.job.task.DummyTask; +import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; +import org.thingsboard.server.common.data.job.task.DummyTaskResult; +import org.thingsboard.server.common.data.job.task.Task; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.util.Collections; import java.util.List; @@ -34,7 +35,7 @@ import java.util.function.Consumer; public class DummyJobProcessor implements JobProcessor { @Override - public int process(Job job, Consumer taskConsumer) throws Exception { + public int process(Job job, Consumer> taskConsumer) throws Exception { DummyJobConfiguration configuration = job.getConfiguration(); if (configuration.getGeneralError() != null) { for (int number = 1; number <= configuration.getSubmittedTasksBeforeGeneralError(); number++) { @@ -63,15 +64,15 @@ public class DummyJobProcessor implements JobProcessor { } @Override - public void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception { - for (TaskFailure failure : failures) { - DummyTaskFailure taskFailure = (DummyTaskFailure) failure; - taskConsumer.accept(createTask(job, job.getConfiguration(), taskFailure.getNumber(), taskFailure.isFailAlways() ? - List.of(taskFailure.getError()) : Collections.emptyList(), taskFailure.isFailAlways())); + public void reprocess(Job job, List taskFailures, Consumer> taskConsumer) throws Exception { + for (TaskResult taskFailure : taskFailures) { + DummyTaskFailure failure = ((DummyTaskResult) taskFailure).getFailure(); + taskConsumer.accept(createTask(job, job.getConfiguration(), failure.getNumber(), failure.isFailAlways() ? + List.of(failure.getError()) : Collections.emptyList(), failure.isFailAlways())); } } - private Task createTask(Job job, DummyJobConfiguration configuration, int number, List errors, boolean failAlways) { + private DummyTask createTask(Job job, DummyJobConfiguration configuration, int number, List errors, boolean failAlways) { return DummyTask.builder() .tenantId(job.getTenantId()) .jobId(job.getId()) diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index da2c75d166..301d4bf6eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -17,17 +17,17 @@ package org.thingsboard.server.service.job; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.Task; -import org.thingsboard.server.common.data.job.TaskFailure; +import org.thingsboard.server.common.data.job.task.Task; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.util.List; import java.util.function.Consumer; public interface JobProcessor { - int process(Job job, Consumer taskConsumer) throws Exception; + int process(Job job, Consumer> taskConsumer) throws Exception; - void reprocess(Job job, List failures, Consumer taskConsumer) throws Exception; + void reprocess(Job job, List taskFailures, Consumer> taskConsumer) throws Exception; JobType getType(); diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 361dbfbde1..5178461746 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -17,16 +17,17 @@ package org.thingsboard.server.service.job.task; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.job.DummyTask; +import org.thingsboard.server.common.data.job.task.DummyTask; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.queue.task.TaskProcessor; @Component @RequiredArgsConstructor -public class DummyTaskProcessor extends TaskProcessor { +public class DummyTaskProcessor extends TaskProcessor { @Override - public Void process(DummyTask task) throws Exception { + public DummyTaskResult process(DummyTask task) throws Exception { if (task.getProcessingTimeMs() > 0) { Thread.sleep(task.getProcessingTimeMs()); } @@ -37,7 +38,7 @@ public class DummyTaskProcessor extends TaskProcessor { String error = task.getErrors().get(task.getAttempt() - 1); throw new RuntimeException(error); } - return null; + return DummyTaskResult.success(); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index f9881d0248..d7d22a2859 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -26,11 +26,12 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; -import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; +import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobService; @@ -101,7 +102,7 @@ public class JobManagerTest extends AbstractControllerTest { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); - assertThat(job.getResult().getFailures()).isEmpty(); + assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); } @@ -131,8 +132,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks); - assertThat(jobResult.getFailures().get(0).getError()).isEqualTo("error3"); // last error - assertThat(jobResult.getFailures().get(1).getError()).isEqualTo("error3"); // last error + assertThat(((DummyTaskResult) jobResult.getResults().get(0)).getFailure().getError()).isEqualTo("error3"); // last error + assertThat(((DummyTaskResult) jobResult.getResults().get(1)).getFailure().getError()).isEqualTo("error3"); // last error assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); }); } @@ -353,7 +354,7 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); } @@ -367,7 +368,7 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getSuccessfulCount()).isEqualTo(totalTasksCount); assertThat(job.getResult().getFailedCount()).isZero(); assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount); - assertThat(job.getResult().getFailures()).isEmpty(); + assertThat(job.getResult().getResults()).isEmpty(); }); } @@ -400,7 +401,7 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); } @@ -417,7 +418,7 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = (DummyTaskFailure) jobResult.getFailures().get(i); + DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); assertThat(failure.isFailAlways()).isTrue(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 7a2eccd42a..8aed4adbe5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.io.Serializable; import java.util.List; @@ -32,7 +33,7 @@ import java.util.List; @Data public abstract class JobConfiguration implements Serializable { - private List toReprocess; + private List toReprocess; public abstract JobType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index bf5e5f2c56..4e4787bbe5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.io.Serializable; import java.util.ArrayList; @@ -40,7 +41,7 @@ public abstract class JobResult implements Serializable { private int failedCount; private int discardedCount; private Integer totalCount = null; // set when all tasks are submitted - private List failures = new ArrayList<>(); + private List results = new ArrayList<>(); private String generalError; private long cancellationTs; @@ -50,6 +51,19 @@ public abstract class JobResult implements Serializable { return successfulCount + failedCount + discardedCount; } + public void processTaskResult(TaskResult taskResult) { + if (taskResult.isSuccess()) { + successfulCount++; + } else if (taskResult.isDiscarded()) { + discardedCount++; + } else { + failedCount++; + if (results.size() < 1000) { // preserving only first 1000 errors, not reprocessing if there are more failures + results.add(taskResult); + } + } + } + public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java index 9d2c3d9be2..dc3e265f2d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.job; import lombok.Data; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.util.ArrayList; import java.util.List; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java similarity index 63% rename from common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java rename to common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java index ac15dc63cc..8c3ed31e50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java @@ -13,22 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.job; +package org.thingsboard.server.common.data.job.task; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.job.JobType; import java.util.List; +import java.util.Optional; @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @SuperBuilder @ToString(callSuper = true) -public class DummyTask extends Task { +public class DummyTask extends Task { private int number; private long processingTimeMs; @@ -41,8 +43,17 @@ public class DummyTask extends Task { } @Override - public TaskFailure toFailure(Throwable error) { - return new DummyTaskFailure(number, failAlways, error.getMessage()); + public DummyTaskResult toResult(boolean discarded, Optional error) { + var result = DummyTaskResult.builder(); + result.discarded(discarded); + if (error.isPresent()) { + result.failure(DummyTaskFailure.builder() + .error(error.map(Throwable::getMessage).orElse(null)) + .number(number) + .failAlways(failAlways) + .build()); + } + return result.build(); } @Override @@ -51,24 +62,14 @@ public class DummyTask extends Task { } @Data - @EqualsAndHashCode(callSuper = true) @NoArgsConstructor - public static class DummyTaskFailure extends TaskFailure { + @EqualsAndHashCode(callSuper = true) + @SuperBuilder + public static class DummyTaskFailure extends TaskFailure { // todo: do we need separate structure? private int number; private boolean failAlways; - public DummyTaskFailure(int number, boolean failAlways, String error) { - super(error); - this.number = number; - this.failAlways = failAlways; - } - - @Override - public JobType getJobType() { - return JobType.DUMMY; - } - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java new file mode 100644 index 0000000000..7c36636181 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -0,0 +1,48 @@ +/** + * 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.common.data.job.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +public class DummyTaskResult extends TaskResult { + + private static final DummyTaskResult SUCCESS = new DummyTaskResult(true); + + private DummyTaskFailure failure; + + public DummyTaskResult(boolean success) { + super(success); + } + + public static DummyTaskResult success() { + return SUCCESS; + } + + @Override + public JobType getJobType() { + return JobType.DUMMY; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java rename to common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java index ad7c6b62df..5cf39ec6fb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.job; +package org.thingsboard.server.common.data.job.task; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -25,6 +25,9 @@ import lombok.Data; import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobType; + +import java.util.Optional; @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -34,7 +37,7 @@ import org.thingsboard.server.common.data.id.TenantId; }) @SuperBuilder @AllArgsConstructor -public abstract class Task { +public abstract class Task { private TenantId tenantId; private JobId jobId; @@ -48,7 +51,7 @@ public abstract class Task { @JsonIgnore public abstract Object getKey(); - public abstract TaskFailure toFailure(Throwable error); + public abstract R toResult(boolean discarded, Optional error); public abstract JobType getJobType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskFailure.java similarity index 79% rename from common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java rename to common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskFailure.java index 0ee9a2b477..ce22a08269 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskFailure.java @@ -13,21 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.job; +package org.thingsboard.server.common.data.job.task; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data @AllArgsConstructor @NoArgsConstructor -@Builder -public class TaskResult { +@SuperBuilder +public abstract class TaskFailure { - private boolean success; - private boolean discarded; - private TaskFailure failure; + private String error; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java similarity index 74% rename from common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java rename to common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 7a04db8188..40f0763e3a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/TaskFailure.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.job; +package org.thingsboard.server.common.data.job.task; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -22,19 +22,26 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.job.DummyTask.DummyTaskFailure; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.job.JobType; @Data @AllArgsConstructor @NoArgsConstructor +@SuperBuilder @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ - @Type(name = "DUMMY", value = DummyTaskFailure.class) + @Type(name = "DUMMY", value = DummyTaskResult.class) }) -public abstract class TaskFailure { +public abstract class TaskResult { - private String error; + private boolean success; + private boolean discarded; + + public TaskResult(boolean success) { + this.success = success; + } public abstract JobType getJobType(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java index 8d69f5781b..c3780f15e7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -22,7 +22,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 57dd46a6e5..25e9d8807d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -25,8 +25,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.Task; -import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.task.Task; +import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; @@ -37,13 +37,14 @@ import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public abstract class TaskProcessor { +public abstract class TaskProcessor, R extends TaskResult> { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -98,16 +99,17 @@ public abstract class TaskProcessor { private void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { for (TbProtoQueueMsg msg : msgs) { try { - Task task = JacksonUtil.fromString(msg.getValue().getValue(), Task.class); + @SuppressWarnings("unchecked") + T task = (T) JacksonUtil.fromString(msg.getValue().getValue(), Task.class); if (discardedJobs.contains(task.getJobId().getId())) { log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); - reportCancelled(task); + reportTaskDiscarded(task); continue; } else if (deletedTenants.contains(task.getTenantId().getId())) { log.info("Skipping task '{}' for deleted tenant {}", task.getKey(), task.getTenantId()); continue; } - processTask((T) task); + processTask(task); } catch (InterruptedException e) { throw e; } catch (Exception e) { @@ -121,8 +123,8 @@ public abstract class TaskProcessor { task.setAttempt(task.getAttempt() + 1); log.info("Processing task: {}", task); try { - process(task); - reportSuccess(task); + R result = process(task); + reportTaskResult(task, result); } catch (InterruptedException e) { throw e; } catch (Exception e) { @@ -130,31 +132,22 @@ public abstract class TaskProcessor { if (task.getAttempt() <= task.getRetries()) { processTask(task); } else { - reportFailure(task, e); + reportTaskFailure(task, e); } } } public abstract R process(T task) throws Exception; - private void reportSuccess(Task task) { - TaskResult result = TaskResult.builder() - .success(true) - .build(); - statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); + private void reportTaskFailure(T task, Throwable error) { + reportTaskResult(task, task.toResult(false, Optional.of(error))); } - private void reportFailure(Task task, Throwable error) { - TaskResult result = TaskResult.builder() - .failure(task.toFailure(error)) - .build(); - statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); + private void reportTaskDiscarded(T task) { + reportTaskResult(task, task.toResult(true, Optional.empty())); } - private void reportCancelled(Task task) { - TaskResult result = TaskResult.builder() - .discarded(true) - .build(); + private void reportTaskResult(T task, R result) { statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 3e511ccfe4..07b5aeadf9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -29,8 +29,7 @@ import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.TaskFailure; -import org.thingsboard.server.common.data.job.TaskResult; +import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -118,17 +117,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi boolean publishEvent = false; for (TaskResult taskResult : jobStats.getTaskResults()) { - if (taskResult.isSuccess()) { - result.setSuccessfulCount(result.getSuccessfulCount() + 1); - } else if (taskResult.isDiscarded()) { - result.setDiscardedCount(result.getDiscardedCount() + 1); - } else { - TaskFailure failure = taskResult.getFailure(); - result.setFailedCount(result.getFailedCount() + 1); - if (result.getFailures().size() < 1000) { // preserving only first 1000 errors, not reprocessing if there are more failures - result.getFailures().add(failure); - } - } + result.processTaskResult(taskResult); if (result.getCancellationTs() > 0) { if (!taskResult.isDiscarded() && System.currentTimeMillis() > result.getCancellationTs()) { From 950d1d85c4461546d274dd855707c90af43b6fde Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 1 May 2025 15:02:28 +0300 Subject: [PATCH 16/44] Task results refactoring --- .../server/service/job/DummyJobProcessor.java | 2 +- .../server/service/job/JobManagerTest.java | 2 +- .../common/data/job/task/DummyTask.java | 30 ++++------------- .../common/data/job/task/DummyTaskResult.java | 33 +++++++++++++++---- .../server/common/data/job/task/Task.java | 6 ++-- .../common/data/job/task/TaskResult.java | 4 --- .../server/queue/task/TaskProcessor.java | 7 ++-- 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java index 64148af259..e1b91e606a 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -21,8 +21,8 @@ import org.thingsboard.server.common.data.job.DummyJobConfiguration; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTask; -import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.task.DummyTaskResult; +import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index d7d22a2859..ee1eee6531 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -30,8 +30,8 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; import org.thingsboard.server.common.data.job.task.DummyTaskResult; +import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobService; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java index 8c3ed31e50..39e1306597 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java @@ -23,7 +23,6 @@ import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.job.JobType; import java.util.List; -import java.util.Optional; @Data @NoArgsConstructor @@ -43,33 +42,18 @@ public class DummyTask extends Task { } @Override - public DummyTaskResult toResult(boolean discarded, Optional error) { - var result = DummyTaskResult.builder(); - result.discarded(discarded); - if (error.isPresent()) { - result.failure(DummyTaskFailure.builder() - .error(error.map(Throwable::getMessage).orElse(null)) - .number(number) - .failAlways(failAlways) - .build()); - } - return result.build(); + public DummyTaskResult toFailed(Throwable error) { + return DummyTaskResult.failed(this, error); } @Override - public JobType getJobType() { - return JobType.DUMMY; + public DummyTaskResult toDiscarded() { + return DummyTaskResult.discarded(); } - @Data - @NoArgsConstructor - @EqualsAndHashCode(callSuper = true) - @SuperBuilder - public static class DummyTaskFailure extends TaskFailure { // todo: do we need separate structure? - - private int number; - private boolean failAlways; - + @Override + public JobType getJobType() { + return JobType.DUMMY; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java index 7c36636181..cd68b4d248 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.job.JobType; -import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; @Data @EqualsAndHashCode(callSuper = true) @@ -28,21 +27,43 @@ import org.thingsboard.server.common.data.job.task.DummyTask.DummyTaskFailure; @SuperBuilder public class DummyTaskResult extends TaskResult { - private static final DummyTaskResult SUCCESS = new DummyTaskResult(true); + private static final DummyTaskResult SUCCESS = DummyTaskResult.builder().success(true).build(); + private static final DummyTaskResult DISCARDED = DummyTaskResult.builder().discarded(true).build(); private DummyTaskFailure failure; - public DummyTaskResult(boolean success) { - super(success); - } - public static DummyTaskResult success() { return SUCCESS; } + public static DummyTaskResult failed(DummyTask task, Throwable error) { + DummyTaskResult result = new DummyTaskResult(); + result.setFailure(DummyTaskFailure.builder() + .error(error.getMessage()) + .number(task.getNumber()) + .failAlways(task.isFailAlways()) + .build()); + return result; + } + + public static DummyTaskResult discarded() { + return DISCARDED; + } + @Override public JobType getJobType() { return JobType.DUMMY; } + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + @SuperBuilder + public static class DummyTaskFailure extends TaskFailure { + + private int number; + private boolean failAlways; + + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java index 5cf39ec6fb..624c5e90f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java @@ -27,8 +27,6 @@ import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.JobType; -import java.util.Optional; - @Data @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @@ -51,7 +49,9 @@ public abstract class Task { @JsonIgnore public abstract Object getKey(); - public abstract R toResult(boolean discarded, Optional error); + public abstract R toFailed(Throwable error); + + public abstract R toDiscarded(); public abstract JobType getJobType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 40f0763e3a..8c4667e1be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -39,10 +39,6 @@ public abstract class TaskResult { private boolean success; private boolean discarded; - public TaskResult(boolean success) { - this.success = success; - } - public abstract JobType getJobType(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 25e9d8807d..643e7b27bc 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -37,7 +37,6 @@ import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -140,11 +139,13 @@ public abstract class TaskProcessor, R extends TaskResult> { public abstract R process(T task) throws Exception; private void reportTaskFailure(T task, Throwable error) { - reportTaskResult(task, task.toResult(false, Optional.of(error))); + R taskResult = task.toFailed(error); + reportTaskResult(task, taskResult); } private void reportTaskDiscarded(T task) { - reportTaskResult(task, task.toResult(true, Optional.empty())); + R taskResult = task.toDiscarded(); + reportTaskResult(task, taskResult); } private void reportTaskResult(T task, R result) { From f4cd471082f864c8ad7f8a1d3c789859d5843d58 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 12:26:23 +0300 Subject: [PATCH 17/44] Notification on job finish --- .../entitiy/EntityStateSourcingListener.java | 2 +- .../server/service/job/DefaultJobManager.java | 65 +++++++++++++++---- .../server/service/job/JobProcessor.java | 2 + .../server/service/job/JobManagerTest.java | 33 +++++++++- .../common/data/job/DummyJobResult.java | 8 +++ .../server/common/data/job/JobResult.java | 3 + .../server/common/data/job/JobType.java | 9 ++- .../server/dao/job/DefaultJobService.java | 4 +- 8 files changed, 108 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index f70354c3a8..f56cc3e952 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -304,7 +304,7 @@ public class EntityStateSourcingListener { private void onJobUpdate(Job job) { jobManager.onJobUpdate(job); - if (job.getResult().getCancellationTs() > 0 || job.getStatus().isOneOf(JobStatus.FAILED)) { + if (job.getResult().getCancellationTs() > 0 || (job.getStatus().isOneOf(JobStatus.FAILED) && job.getResult().getGeneralError() != null)) { // task processors will add this job to the list of discarded tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 3d601a9358..3da2e2a9ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -18,12 +18,12 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -33,8 +33,12 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; +import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; +import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueCallback; @@ -65,6 +69,7 @@ public class DefaultJobManager implements JobManager { private final JobService jobService; private final JobStatsService jobStatsService; + private final NotificationCenter notificationCenter; private final Map jobProcessors; private final Map>> taskProducers; private final QueueConsumerManager> jobStatsConsumer; @@ -74,9 +79,11 @@ public class DefaultJobManager implements JobManager { @Value("${queue.tasks.stats.processing_interval_ms:5000}") private int statsProcessingInterval; - public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, TbCoreQueueFactory queueFactory, List jobProcessors) { + public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, NotificationCenter notificationCenter, + TbCoreQueueFactory queueFactory, List jobProcessors) { this.jobService = jobService; this.jobStatsService = jobStatsService; + this.notificationCenter = notificationCenter; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); @@ -104,10 +111,29 @@ public class DefaultJobManager implements JobManager { @Override public void onJobUpdate(Job job) { - if (job.getStatus() == JobStatus.PENDING) { - executor.execute(() -> { - processJob(job); - }); + JobStatus status = job.getStatus(); + switch (status) { + case PENDING -> { + executor.execute(() -> { + try { + processJob(job); + } catch (Throwable e) { + log.error("Failed to process job update: {}", job, e); + } + }); + } + case COMPLETED, FAILED -> { + executor.execute(() -> { + try { + if (status == JobStatus.COMPLETED) { + getJobProcessor(job.getType()).onJobCompleted(job); + } + sendJobFinishedNotification(job); + } catch (Throwable e) { + log.error("Failed to process job update: {}", job, e); + } + }); + } } } @@ -115,7 +141,7 @@ public class DefaultJobManager implements JobManager { TenantId tenantId = job.getTenantId(); JobId jobId = job.getId(); try { - JobProcessor processor = jobProcessors.get(job.getType()); + JobProcessor processor = getJobProcessor(job.getType()); List toReprocess = job.getConfiguration().getToReprocess(); if (toReprocess == null) { int tasksCount = processor.process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted @@ -127,11 +153,7 @@ public class DefaultJobManager implements JobManager { } } catch (Throwable e) { log.error("[{}][{}][{}] Failed to submit tasks", tenantId, jobId, job.getType(), e); - try { - jobService.markAsFailed(tenantId, jobId, ExceptionUtils.getStackTrace(e)); - } catch (Throwable e2) { - log.error("[{}][{}] Failed to mark job as failed", tenantId, jobId, e2); - } + jobService.markAsFailed(tenantId, jobId, e.getMessage()); } } @@ -224,6 +246,25 @@ public class DefaultJobManager implements JobManager { Thread.sleep(statsProcessingInterval); // todo: test with bigger interval } + private void sendJobFinishedNotification(Job job) { + NotificationTemplate template = DefaultNotifications.DefaultNotification.builder() + .name("Job finished") + .subject("${type} ${status}") + .text("${description} ${status}: ${result}") + .build().toTemplate(); + GeneralNotificationInfo info = new GeneralNotificationInfo(Map.of( + "type", job.getType().getTitle(), + "description", job.getDescription(), + "status", job.getStatus().name().toLowerCase(), + "result", job.getResult().getDescription() + )); + notificationCenter.sendGeneralWebNotification(job.getTenantId(), new TenantAdministratorsFilter(), template, info); + } + + private JobProcessor getJobProcessor(JobType jobType) { + return jobProcessors.get(jobType); + } + @PreDestroy private void destroy() { jobStatsConsumer.stop(); diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index 301d4bf6eb..16ef5e404f 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -29,6 +29,8 @@ public interface JobProcessor { void reprocess(Job job, List taskFailures, Consumer> taskConsumer) throws Exception; + default void onJobCompleted(Job job) {} + JobType getType(); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index ee1eee6531..39c87dfd70 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.job; import org.assertj.core.api.Assertions; +import org.assertj.core.api.ThrowingConsumer; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; +import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobService; @@ -85,7 +87,7 @@ public class JobManagerTest extends AbstractControllerTest { .tenantId(tenantId) .type(JobType.DUMMY) .key("test-job") - .description("test job") + .description("Test job") .configuration(DummyJobConfiguration.builder() .successfulTasksCount(tasksCount) .taskProcessingTimeMs(1000) @@ -105,6 +107,11 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); + + checkJobNotification(notification -> { + assertThat(notification.getSubject()).isEqualTo("Dummy job completed"); + assertThat(notification.getText()).isEqualTo("Test job completed: 5/5 successful, 0 failed"); + }); } @Test @@ -115,7 +122,7 @@ public class JobManagerTest extends AbstractControllerTest { .tenantId(tenantId) .type(JobType.DUMMY) .key("test-job") - .description("test job") + .description("Test job") .configuration(DummyJobConfiguration.builder() .successfulTasksCount(successfulTasks) .failedTasksCount(failedTasks) @@ -136,6 +143,11 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(((DummyTaskResult) jobResult.getResults().get(1)).getFailure().getError()).isEqualTo("error3"); // last error assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); }); + + checkJobNotification(notification -> { + assertThat(notification.getSubject()).isEqualTo("Dummy job failed"); + assertThat(notification.getText()).isEqualTo("Test job failed: 3/5 successful, 2 failed"); + }); } @Test @@ -311,7 +323,7 @@ public class JobManagerTest extends AbstractControllerTest { .tenantId(tenantId) .type(JobType.DUMMY) .key("test-job") - .description("test job") + .description("Test job") .configuration(DummyJobConfiguration.builder() .generalError("Some error while submitting tasks") .submittedTasksBeforeGeneralError(submittedTasks) @@ -326,6 +338,11 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getDiscardedCount()).isBetween(1, submittedTasks); assertThat(job.getResult().getTotalCount()).isNull(); }); + + checkJobNotification(notification -> { + assertThat(notification.getSubject()).isEqualTo("Dummy job failed"); + assertThat(notification.getText()).isEqualTo("Test job failed: Some error while submitting tasks"); + }); } @Test @@ -426,6 +443,16 @@ public class JobManagerTest extends AbstractControllerTest { }); } + private void checkJobNotification(ThrowingConsumer assertFunction) { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Notification notification = getMyNotifications(true, 100).stream() + .findFirst().orElse(null); + assertThat(notification).isNotNull(); + + assertFunction.accept(notification); + }); + } + // todo: job with zero tasks } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java index 031a733d51..3a9aabae76 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java @@ -17,6 +17,14 @@ package org.thingsboard.server.common.data.job; public class DummyJobResult extends JobResult { + @Override + public String getDescription() { + if (getGeneralError() != null) { + return getGeneralError(); + } + return getSuccessfulCount() + "/" + getTotalCount() + " successful, " + getFailedCount() + " failed"; + } + @Override public JobType getJobType() { return JobType.DUMMY; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 4e4787bbe5..534e3587bb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -64,6 +64,9 @@ public abstract class JobResult implements Serializable { } } + @JsonIgnore + public abstract String getDescription(); + public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java index 9e8e9fa7e5..c2c461d12b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobType.java @@ -15,9 +15,16 @@ */ package org.thingsboard.server.common.data.job; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter public enum JobType { - DUMMY; + DUMMY("Dummy job"); + + private final String title; public String getTasksTopic() { return "tasks." + name().toLowerCase(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 07b5aeadf9..73e0c5dff4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -117,7 +117,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi boolean publishEvent = false; for (TaskResult taskResult : jobStats.getTaskResults()) { - result.processTaskResult(taskResult); + result.processTaskResult(taskResult); if (result.getCancellationTs() > 0) { if (!taskResult.isDiscarded() && System.currentTimeMillis() > result.getCancellationTs()) { @@ -134,8 +134,10 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(CANCELLED); } else if (result.getFailedCount() > 0) { job.setStatus(FAILED); + publishEvent = true; } else { job.setStatus(COMPLETED); + publishEvent = true; } } } From 03845450ef6508c2102beded1eb1cc030d6fce3d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 12:40:40 +0300 Subject: [PATCH 18/44] Truncate notification table on test finish --- .../org/thingsboard/server/controller/AbstractWebTest.java | 5 +++++ .../service/notification/AbstractNotificationApiTest.java | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index d580a6b12a..7a96384be5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -48,6 +48,7 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.web.MockMultipartFile; @@ -281,6 +282,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected InMemoryStorage storage; + protected JdbcTemplate jdbcTemplate; + @MockBean protected CfRocksDb cfRocksDb; @@ -392,6 +395,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); + jdbcTemplate.execute("TRUNCATE TABLE notification"); + log.info("Executed web test teardown"); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index b7e4013bb9..f238a1bbc1 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -21,7 +21,6 @@ import org.junit.After; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.util.Pair; -import org.springframework.jdbc.core.JdbcTemplate; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.server.common.data.User; @@ -89,8 +88,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest protected SqlPartitioningRepository partitioningRepository; @Autowired protected DefaultNotifications defaultNotifications; - @Autowired - private JdbcTemplate jdbcTemplate; public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; @@ -101,7 +98,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID); notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID); notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID); - jdbcTemplate.execute("TRUNCATE TABLE notification"); partitioningRepository.cleanupPartitionsCache("notification", Long.MAX_VALUE, 0); notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID); } From 09e334660fb70b2b8c5c1f03f2c0f925b0ed6a1a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 13:02:21 +0300 Subject: [PATCH 19/44] Truncate notification table on test finish --- .../java/org/thingsboard/server/controller/AbstractWebTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 7a96384be5..054c2fbe59 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -282,6 +282,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected InMemoryStorage storage; + @Autowired protected JdbcTemplate jdbcTemplate; @MockBean From e59460b42bbcf69289aa2bdf8851e98472e911d1 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 15:14:00 +0300 Subject: [PATCH 20/44] Task processors partitioning --- .../server/service/job/DefaultJobManager.java | 15 +++-- .../service/job/task/DummyTaskProcessor.java | 4 +- .../src/main/resources/thingsboard.yml | 11 +++- .../server/service/job/JobManagerTest.java | 9 ++- .../server/service/job/TestTaskProcessor.java | 23 ++++++++ .../server/common/msg/queue/ServiceType.java | 3 +- common/proto/src/main/proto/queue.proto | 1 + .../DefaultTbServiceInfoProvider.java | 17 +++++- .../queue/discovery/HashPartitionService.java | 17 +++++- .../server/queue/task/TaskProcessor.java | 47 ++++++++------- .../queue/task/TaskProcessorExecutors.java | 59 +++++++++++++++++++ 11 files changed, 167 insertions(+), 39 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorExecutors.java diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 3da2e2a9ab..79312ba608 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo; import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.notification.DefaultNotifications; @@ -47,6 +48,7 @@ import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.queue.util.AfterStartUp; @@ -70,20 +72,22 @@ public class DefaultJobManager implements JobManager { private final JobService jobService; private final JobStatsService jobStatsService; private final NotificationCenter notificationCenter; + private final PartitionService partitionService; private final Map jobProcessors; private final Map>> taskProducers; private final QueueConsumerManager> jobStatsConsumer; private final ExecutorService executor; private final ExecutorService consumerExecutor; - @Value("${queue.tasks.stats.processing_interval_ms:5000}") + @Value("${queue.tasks.stats.processing_interval_ms:1000}") private int statsProcessingInterval; public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, NotificationCenter notificationCenter, - TbCoreQueueFactory queueFactory, List jobProcessors) { + PartitionService partitionService, TbCoreQueueFactory queueFactory, List jobProcessors) { this.jobService = jobService; this.jobStatsService = jobStatsService; this.notificationCenter = notificationCenter; + this.partitionService = partitionService; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); @@ -199,8 +203,8 @@ public class DefaultJobManager implements JobManager { .build(); TbQueueProducer> producer = taskProducers.get(task.getJobType()); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(task.getTenantId().getId(), taskProto); // one job at a time for a given tenant - producer.send(TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(), msg, new TbQueueCallback() { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TASK_PROCESSOR, task.getJobType().name(), task.getTenantId(), task.getTenantId()); // one job at a time for a given tenant + producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), taskProto), new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { log.trace("Submitted task: {}", task); @@ -249,7 +253,7 @@ public class DefaultJobManager implements JobManager { private void sendJobFinishedNotification(Job job) { NotificationTemplate template = DefaultNotifications.DefaultNotification.builder() .name("Job finished") - .subject("${type} ${status}") + .subject("${type} task ${status}") .text("${description} ${status}: ${result}") .build().toTemplate(); GeneralNotificationInfo info = new GeneralNotificationInfo(Map.of( @@ -258,6 +262,7 @@ public class DefaultJobManager implements JobManager { "status", job.getStatus().name().toLowerCase(), "result", job.getResult().getDescription() )); + // todo: button to see details (forward to jobs page) notificationCenter.sendGeneralWebNotification(job.getTenantId(), new TenantAdministratorsFilter(), template, info); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 5178461746..564142ff2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -16,13 +16,11 @@ package org.thingsboard.server.service.job.task; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.job.task.DummyTask; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.job.task.DummyTask; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.queue.task.TaskProcessor; -@Component @RequiredArgsConstructor public class DummyTaskProcessor extends TaskProcessor { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 322b037e2d..7a7e117120 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1628,6 +1628,11 @@ queue: - key: max.poll.records # Max poll records for edqs.state topic value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}" + tasks: + # Key-value properties for Kafka consumer for tasks topics + - key: max.poll.records + # Max poll records for tasks topics + value: "${TB_QUEUE_KAFKA_TASKS_MAX_POLL_RECORDS:1}" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms @@ -1668,7 +1673,7 @@ queue: # Kafka properties for EDQS state topic (infinite retention, compaction) edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1;cleanup.policy:compact}" # Kafka properties for tasks topics - tasks: "${TB_QUEUE_KAFKA_TASKS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" + tasks: "${TB_QUEUE_KAFKA_TASKS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:1;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1884,9 +1889,11 @@ queue: # Statistics printing interval for Edge services print-interval-ms: "${TB_QUEUE_EDGE_STATS_PRINT_INTERVAL_MS:60000}" tasks: + # Partitions count for tasks queues + partitions: "${TB_QUEUE_TASKS_PARTITIONS:12}" stats: # Interval in milliseconds to process job stats - processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:5000}" + processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" # Event configuration parameters event: diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 39c87dfd70..796a38ce4f 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -39,7 +39,6 @@ import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.task.JobStatsService; -import org.thingsboard.server.service.job.task.DummyTaskProcessor; import java.util.ArrayList; import java.util.List; @@ -66,7 +65,7 @@ public class JobManagerTest extends AbstractControllerTest { private JobService jobService; @SpyBean - private DummyTaskProcessor taskProcessor; + private TestTaskProcessor taskProcessor; @SpyBean private JobStatsService jobStatsService; @@ -109,7 +108,7 @@ public class JobManagerTest extends AbstractControllerTest { }); checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job completed"); + assertThat(notification.getSubject()).isEqualTo("Dummy job task completed"); assertThat(notification.getText()).isEqualTo("Test job completed: 5/5 successful, 0 failed"); }); } @@ -145,7 +144,7 @@ public class JobManagerTest extends AbstractControllerTest { }); checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job failed"); + assertThat(notification.getSubject()).isEqualTo("Dummy job task failed"); assertThat(notification.getText()).isEqualTo("Test job failed: 3/5 successful, 2 failed"); }); } @@ -340,7 +339,7 @@ public class JobManagerTest extends AbstractControllerTest { }); checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job failed"); + assertThat(notification.getSubject()).isEqualTo("Dummy job task failed"); assertThat(notification.getText()).isEqualTo("Test job failed: Some error while submitting tasks"); }); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java b/application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java new file mode 100644 index 0000000000..fdaec648b5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java @@ -0,0 +1,23 @@ +/** + * 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.job; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.service.job.task.DummyTaskProcessor; + +@Component +public class TestTaskProcessor extends DummyTaskProcessor { +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java index f31fdfa7a8..8fd535891c 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java @@ -27,7 +27,8 @@ public enum ServiceType { TB_TRANSPORT("TB Transport"), JS_EXECUTOR("JS Executor"), TB_VC_EXECUTOR("TB VC Executor"), - EDQS("TB Entity Data Query Service"); + EDQS("TB Entity Data Query Service"), + TASK_PROCESSOR("Task Processor"); private final String label; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index b4d436a32a..1cc507c5b3 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -88,6 +88,7 @@ message ServiceInfo { SystemInfoProto systemInfo = 10; repeated string assignedTenantProfiles = 11; string label = 12; + repeated string taskTypes = 13; } message SystemInfoProto { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 609d3f8eee..4325651540 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -24,11 +24,13 @@ import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbTransportService; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.task.TaskProcessor; import org.thingsboard.server.queue.util.AfterContextReady; import java.net.InetAddress; @@ -40,7 +42,12 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import static org.thingsboard.common.util.SystemUtil.*; +import static org.thingsboard.common.util.SystemUtil.getCpuCount; +import static org.thingsboard.common.util.SystemUtil.getCpuUsage; +import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage; +import static org.thingsboard.common.util.SystemUtil.getMemoryUsage; +import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace; +import static org.thingsboard.common.util.SystemUtil.getTotalMemory; @Component @@ -65,7 +72,11 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { @Autowired private ApplicationContext applicationContext; + @Autowired + private List> availableTaskProcessors; + private List serviceTypes; + private List taskTypes; private ServiceInfo serviceInfo; @PostConstruct @@ -91,6 +102,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { edqsConfig.setLabel(serviceId); } } + taskTypes = availableTaskProcessors.stream() + .map(TaskProcessor::getJobType) + .toList(); generateNewServiceInfoWithCurrentSystemInfo(); } @@ -128,6 +142,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { builder.addAllAssignedTenantProfiles(assignedTenantProfiles.stream().map(UUID::toString).collect(Collectors.toList())); } builder.setLabel(edqsConfig.getLabel()); + builder.addAllTaskTypes(taskTypes.stream().map(JobType::name).toList()); return serviceInfo = builder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 7186bf7055..ccbfbc9c79 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -82,6 +83,8 @@ public class HashPartitionService implements PartitionService { private Integer edgePartitions; @Value("${queue.edqs.partitions:12}") private Integer edqsPartitions; + @Value("${queue.tasks.partitions:12}") + private Integer tasksPartitions; @Value("${queue.partitions.hash_function_name:murmur3_128}") private String hashFunctionName; @@ -140,6 +143,12 @@ public class HashPartitionService implements PartitionService { QueueKey edqsKey = new QueueKey(ServiceType.EDQS); partitionSizesMap.put(edqsKey, edqsPartitions); partitionTopicsMap.put(edqsKey, "edqs"); // placeholder, not used + + for (JobType jobType : JobType.values()) { + QueueKey queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, jobType.name()); + partitionSizesMap.put(queueKey, tasksPartitions); + partitionTopicsMap.put(queueKey, jobType.getTasksTopic()); + } } @AfterStartUp(order = AfterStartUp.QUEUE_INFO_INITIALIZATION) @@ -454,8 +463,8 @@ public class HashPartitionService implements PartitionService { if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { partitionSizesMap.keySet().stream() .filter(queueKey -> queueKey.getType() == ServiceType.TB_RULE_ENGINE && - !queueKey.getTenantId().isSysTenantId() && - !newPartitions.containsKey(queueKey)) + !queueKey.getTenantId().isSysTenantId() && + !newPartitions.containsKey(queueKey)) .forEach(removed::add); } removed.forEach(queueKey -> { @@ -675,6 +684,10 @@ public class HashPartitionService implements PartitionService { for (String transportType : instance.getTransportsList()) { tbTransportServicesByType.computeIfAbsent(transportType, t -> new ArrayList<>()).add(instance); } + for (String taskType : instance.getTaskTypesList()) { + QueueKey queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, taskType); + queueServiceList.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(instance); + } } @NotNull diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 643e7b27bc..82f13ce66b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -22,26 +22,27 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; -import org.thingsboard.server.queue.util.AfterStartUp; 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; public abstract class TaskProcessor, R extends TaskResult> { @@ -51,29 +52,35 @@ public abstract class TaskProcessor, R extends TaskResult> { private TaskProcessorQueueFactory queueFactory; @Autowired private JobStatsService statsService; + @Autowired + private TaskProcessorExecutors executors; - private QueueConsumerManager> taskConsumer; - private ExecutorService consumerExecutor; + private QueueKey queueKey; + private MainQueueConsumerManager, QueueConfig> taskConsumer; private final Set deletedTenants = ConcurrentHashMap.newKeySet(); private final Set discardedJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine @PostConstruct public void init() { - consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-consumer")); - taskConsumer = QueueConsumerManager.>builder() // fixme: should be consumer per partition - .name(getJobType().name().toLowerCase() + "-tasks") - .msgPackProcessor(this::processMsgs) // todo: max.poll.records = 1 - .pollInterval(125) - .consumerCreator(() -> queueFactory.createTaskConsumer(getJobType())) - .consumerExecutor(consumerExecutor) + queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, getJobType().name()); + taskConsumer = MainQueueConsumerManager., QueueConfig>builder() + .queueKey(queueKey) + .config(QueueConfig.of(true, 500)) + .msgPackProcessor(this::processMsgs) + .consumerCreator((queueConfig, tpi) -> queueFactory.createTaskConsumer(getJobType())) + .consumerExecutor(executors.getConsumersExecutor()) + .scheduler(executors.getScheduler()) + .taskExecutor(executors.getMgmtExecutor()) .build(); } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void afterStartUp() { - taskConsumer.subscribe(); - taskConsumer.launch(); + @EventListener + public void onPartitionChangeEvent(PartitionChangeEvent event) { + if (event.getServiceType() == ServiceType.TASK_PROCESSOR) { + Set partitions = event.getNewPartitions().get(queueKey); + taskConsumer.update(partitions); + } } @EventListener @@ -95,7 +102,7 @@ public abstract class TaskProcessor, R extends TaskResult> { } } - private void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { + private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig queueConfig) throws Exception { for (TbProtoQueueMsg msg : msgs) { try { @SuppressWarnings("unchecked") @@ -159,7 +166,7 @@ public abstract class TaskProcessor, R extends TaskResult> { @PreDestroy public void destroy() { taskConsumer.stop(); - consumerExecutor.shutdownNow(); + taskConsumer.awaitStop(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorExecutors.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorExecutors.java new file mode 100644 index 0000000000..3aa6a0f004 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorExecutors.java @@ -0,0 +1,59 @@ +/** + * 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.queue.task; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Getter +@Lazy +@Component +public class TaskProcessorExecutors { + + private ExecutorService consumersExecutor; + private ExecutorService mgmtExecutor; + private ScheduledExecutorService scheduler; + + @PostConstruct + private void init() { + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("task-consumer")); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "task-consumer-mgmt"); + scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("task-consumer-scheduler"); + } + + @PreDestroy + private void destroy() { + if (consumersExecutor != null) { + consumersExecutor.shutdownNow(); + } + if (mgmtExecutor != null) { + mgmtExecutor.shutdownNow(); + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + +} From 4c01b3d70a4f504c280696b4fdbadbd44412f7fc Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 16:23:05 +0300 Subject: [PATCH 21/44] Refactoring for task processor queue factories --- .../discovery/HashPartitionServiceTest.java | 8 +- .../DefaultTbServiceInfoProvider.java | 18 ++-- .../queue/discovery/HashPartitionService.java | 35 ++++---- .../InMemoryMonolithQueueFactory.java | 10 --- .../provider/KafkaMonolithQueueFactory.java | 23 ------ .../provider/KafkaTbCoreQueueFactory.java | 23 ------ .../KafkaTbRuleEngineQueueFactory.java | 28 ------- .../queue/provider/TbCoreQueueFactory.java | 2 +- .../provider/TbCoreQueueProducerProvider.java | 8 -- .../provider/TbQueueProducerProvider.java | 3 - .../TbRuleEngineProducerProvider.java | 8 -- .../provider/TbRuleEngineQueueFactory.java | 2 +- .../TbTransportQueueProducerProvider.java | 5 -- .../TbVersionControlProducerProvider.java | 5 -- .../InMemoryTaskProcessorQueueFactory.java | 48 +++++++++++ .../server/queue/task/JobStatsService.java | 10 +-- .../task/KafkaTaskProcessorQueueFactory.java | 82 +++++++++++++++++++ .../server/queue/task/TaskProcessor.java | 1 - .../TaskProcessorQueueFactory.java | 2 +- .../edqs/DummyQueueRoutingInfoService.java | 33 -------- .../edqs/DummyTenantRoutingInfoService.java | 30 ------- 21 files changed, 172 insertions(+), 212 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java rename common/queue/src/main/java/org/thingsboard/server/queue/{provider => task}/TaskProcessorQueueFactory.java (96%) delete mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java delete mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index dace159774..a149ca6dfd 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -49,6 +49,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -419,10 +420,11 @@ public class HashPartitionServiceTest { } private HashPartitionService createPartitionService() { - HashPartitionService partitionService = new HashPartitionService(serviceInfoProvider, - routingInfoService, + HashPartitionService partitionService = new HashPartitionService( applicationEventPublisher, - queueRoutingInfoService, + serviceInfoProvider, + Optional.of(routingInfoService), + Optional.of(queueRoutingInfoService), topicService); ReflectionTestUtils.setField(partitionService, "coreTopic", "tb.core"); ReflectionTestUtils.setField(partitionService, "corePartitions", 10); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 4325651540..f244f99b6b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -66,13 +66,13 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { @Value("${service.rule_engine.assigned_tenant_profiles:}") private Set assignedTenantProfiles; - @Autowired + @Autowired(required = false) private EdqsConfig edqsConfig; @Autowired private ApplicationContext applicationContext; - @Autowired + @Autowired(required = false) private List> availableTaskProcessors; private List serviceTypes; @@ -102,9 +102,13 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { edqsConfig.setLabel(serviceId); } } - taskTypes = availableTaskProcessors.stream() - .map(TaskProcessor::getJobType) - .toList(); + if (CollectionsUtil.isNotEmpty(availableTaskProcessors)) { + taskTypes = availableTaskProcessors.stream() + .map(TaskProcessor::getJobType) + .toList(); + } else { + taskTypes = Collections.emptyList(); + } generateNewServiceInfoWithCurrentSystemInfo(); } @@ -141,7 +145,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { if (CollectionsUtil.isNotEmpty(assignedTenantProfiles)) { builder.addAllAssignedTenantProfiles(assignedTenantProfiles.stream().map(UUID::toString).collect(Collectors.toList())); } - builder.setLabel(edqsConfig.getLabel()); + if (edqsConfig != null) { + builder.setLabel(edqsConfig.getLabel()); + } builder.addAllTaskTypes(taskTypes.stream().map(JobType::name).toList()); return serviceInfo = builder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index ccbfbc9c79..3f81199488 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -19,6 +19,7 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import jakarta.annotation.PostConstruct; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -49,6 +50,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -63,6 +65,7 @@ import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @Service @Slf4j +@RequiredArgsConstructor public class HashPartitionService implements PartitionService { @Value("${queue.core.topic:tb_core}") @@ -90,8 +93,8 @@ public class HashPartitionService implements PartitionService { private final ApplicationEventPublisher applicationEventPublisher; private final TbServiceInfoProvider serviceInfoProvider; - private final TenantRoutingInfoService tenantRoutingInfoService; - private final QueueRoutingInfoService queueRoutingInfoService; + private final Optional tenantRoutingInfoService; + private final Optional queueRoutingInfoService; private final TopicService topicService; protected volatile ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); @@ -108,18 +111,6 @@ public class HashPartitionService implements PartitionService { private HashFunction hashFunction; - public HashPartitionService(TbServiceInfoProvider serviceInfoProvider, - TenantRoutingInfoService tenantRoutingInfoService, - ApplicationEventPublisher applicationEventPublisher, - QueueRoutingInfoService queueRoutingInfoService, - TopicService topicService) { - this.serviceInfoProvider = serviceInfoProvider; - this.tenantRoutingInfoService = tenantRoutingInfoService; - this.applicationEventPublisher = applicationEventPublisher; - this.queueRoutingInfoService = queueRoutingInfoService; - this.topicService = topicService; - } - @PostConstruct public void init() { this.hashFunction = forName(hashFunctionName); @@ -178,6 +169,10 @@ public class HashPartitionService implements PartitionService { } private List getQueueRoutingInfos() { + if (queueRoutingInfoService.isEmpty()) { + return Collections.emptyList(); + } + List queueRoutingInfoList; String serviceType = serviceInfoProvider.getServiceType(); @@ -188,7 +183,7 @@ public class HashPartitionService implements PartitionService { if (getQueuesRetries > 0) { log.info("Try to get queue routing info."); try { - queueRoutingInfoList = queueRoutingInfoService.getAllQueuesRoutingInfo(); + queueRoutingInfoList = queueRoutingInfoService.get().getAllQueuesRoutingInfo(); break; } catch (Exception e) { log.info("Failed to get queues routing info: {}!", e.getMessage()); @@ -204,7 +199,7 @@ public class HashPartitionService implements PartitionService { } } } else { - queueRoutingInfoList = queueRoutingInfoService.getAllQueuesRoutingInfo(); + queueRoutingInfoList = queueRoutingInfoService.get().getAllQueuesRoutingInfo(); } return queueRoutingInfoList; } @@ -638,7 +633,11 @@ public class HashPartitionService implements PartitionService { } private TenantRoutingInfo getRoutingInfo(TenantId tenantId) { - return tenantRoutingInfoMap.computeIfAbsent(tenantId, tenantRoutingInfoService::getRoutingInfo); + if (tenantRoutingInfoService.isPresent()) { + return tenantRoutingInfoMap.computeIfAbsent(tenantId, __ -> tenantRoutingInfoService.get().getRoutingInfo(tenantId)); + } else { + return new TenantRoutingInfo(tenantId, null, false); + } } protected TenantId getIsolatedOrSystemTenantId(ServiceType serviceType, TenantId tenantId) { @@ -702,7 +701,7 @@ public class HashPartitionService implements PartitionService { if (!responsibleServices.isEmpty()) { // if there are any dedicated servers TenantProfileId profileId; if (tenantId != null && !tenantId.isSysTenantId()) { - TenantRoutingInfo routingInfo = tenantRoutingInfoService.getRoutingInfo(tenantId); + TenantRoutingInfo routingInfo = tenantRoutingInfoService.get().getRoutingInfo(tenantId); profileId = routingInfo.getProfileId(); } else { profileId = null; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 9160818278..a164237366 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -265,16 +265,6 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueProducer<>(storage, jobType.getTasksTopic()); } - @Override - public TbQueueConsumer> createTaskConsumer(JobType jobType) { - return new InMemoryTbQueueConsumer<>(storage, jobType.getTasksTopic()); - } - - @Override - public TbQueueProducer> createJobStatsProducer() { - return new InMemoryTbQueueProducer<>(storage, "jobs.stats"); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return new InMemoryTbQueueConsumer<>(storage, "jobs.stats"); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index e4b51eaa1f..b15e60f09d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -656,29 +656,6 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .build(); } - @Override - public TbQueueConsumer> createTaskConsumer(JobType jobType) { - return TbKafkaConsumerTemplate.>builder() - .settings(kafkaSettings) - .topic(topicService.buildTopicName(jobType.getTasksTopic())) - .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) - .admin(tasksAdmin) - .statsService(consumerStatsService) - .build(); - } - - @Override - public TbQueueProducer> createJobStatsProducer() { - return TbKafkaProducerTemplate.>builder() - .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("jobs.stats")) - .settings(kafkaSettings) - .admin(tasksAdmin) - .build(); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 85d1e8be14..0bf6acd830 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -535,29 +535,6 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .build(); } - @Override - public TbQueueConsumer> createTaskConsumer(JobType jobType) { - return TbKafkaConsumerTemplate.>builder() - .settings(kafkaSettings) - .topic(topicService.buildTopicName(jobType.getTasksTopic())) - .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) - .admin(tasksAdmin) - .statsService(consumerStatsService) - .build(); - } - - @Override - public TbQueueProducer> createJobStatsProducer() { - return TbKafkaProducerTemplate.>builder() - .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("jobs.stats")) - .settings(kafkaSettings) - .admin(tasksAdmin) - .build(); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index b2af3671c6..3b67ea4f9f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -22,15 +22,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; -import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -99,7 +96,6 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin cfAdmin; private final TbQueueAdmin cfStateAdmin; private final TbQueueAdmin edqsEventsAdmin; - private final TbQueueAdmin tasksAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -137,7 +133,6 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.cfAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldConfigs()); this.cfStateAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCalculatedFieldStateConfigs()); this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); - this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); } @Override @@ -419,29 +414,6 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { throw new UnsupportedOperationException(); } - @Override - public TbQueueConsumer> createTaskConsumer(JobType jobType) { - return TbKafkaConsumerTemplate.>builder() - .settings(kafkaSettings) - .topic(topicService.buildTopicName(jobType.getTasksTopic())) - .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) - .admin(tasksAdmin) - .statsService(consumerStatsService) - .build(); - } - - @Override - public TbQueueProducer> createJobStatsProducer() { - return TbKafkaProducerTemplate.>builder() - .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("jobs.stats")) - .settings(kafkaSettings) - .admin(tasksAdmin) - .build(); - } - @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 823ebea298..e47354941c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -47,7 +47,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory, TaskProcessorQueueFactory { +public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 9900474a10..98a3d78304 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -18,7 +18,6 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -54,7 +53,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toHousekeeper; private TbQueueProducer> toCalculatedFields; private TbQueueProducer> toCalculatedFieldNotifications; - private TbQueueProducer> jobStatsProducer; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -75,7 +73,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toEdgeEvents = tbQueueProvider.createEdgeEventMsgProducer(); this.toCalculatedFields = tbQueueProvider.createToCalculatedFieldMsgProducer(); this.toCalculatedFieldNotifications = tbQueueProvider.createToCalculatedFieldNotificationMsgProducer(); - this.jobStatsProducer = tbQueueProvider.createJobStatsProducer(); } @Override @@ -143,9 +140,4 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toCalculatedFieldNotifications; } - @Override - public TbQueueProducer> getJobStatsProducer() { - return jobStatsProducer; - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index 428e673fa8..865637b2ff 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.queue.provider; -import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -98,6 +97,4 @@ public interface TbQueueProducerProvider { TbQueueProducer> getCalculatedFieldsNotificationsMsgProducer(); - TbQueueProducer> getJobStatsProducer(); - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 9e77a2d4e7..8e1952fc14 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -18,7 +18,6 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -52,7 +51,6 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toEdgeEvents; private TbQueueProducer> toCalculatedFields; private TbQueueProducer> toCalculatedFieldNotifications; - private TbQueueProducer> jobStatsProducer; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -72,7 +70,6 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toEdgeEvents = tbQueueProvider.createEdgeEventMsgProducer(); this.toCalculatedFields = tbQueueProvider.createToCalculatedFieldMsgProducer(); this.toCalculatedFieldNotifications = tbQueueProvider.createToCalculatedFieldNotificationMsgProducer(); - this.jobStatsProducer = tbQueueProvider.createJobStatsProducer(); } @Override @@ -140,9 +137,4 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { return toCalculatedFieldNotifications; } - @Override - public TbQueueProducer> getJobStatsProducer() { - return jobStatsProducer; - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 83c467c992..18bb6db14a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -41,7 +41,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory, TaskProcessorQueueFactory { +public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index 4472c6157e..cb7e6dd1f4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -122,9 +122,4 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } - @Override - public TbQueueProducer> getJobStatsProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Transport!"); - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 0370f8a4af..85c400d094 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -118,9 +118,4 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } - @Override - public TbQueueProducer> getJobStatsProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java new file mode 100644 index 0000000000..dbe302dfee --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java @@ -0,0 +1,48 @@ +/** + * 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.queue.task; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; +import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; + +@Component +@ConditionalOnExpression("'${queue.type:null}'=='in-memory'") +@RequiredArgsConstructor +public class InMemoryTaskProcessorQueueFactory implements TaskProcessorQueueFactory { + + private final InMemoryStorage storage; + + @Override + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return new InMemoryTbQueueConsumer<>(storage, jobType.getTasksTopic()); + } + + @Override + public TbQueueProducer> createJobStatsProducer() { + return new InMemoryTbQueueProducer<>(storage, "jobs.stats"); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java index c3780f15e7..28d08f593c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.queue.task; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -29,15 +28,17 @@ import org.thingsboard.server.gen.transport.TransportProtos.TaskResultProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @Lazy @Service @Slf4j -@RequiredArgsConstructor public class JobStatsService { - private final TbQueueProducerProvider producerProvider; + private final TbQueueProducer> producer; + + public JobStatsService(TaskProcessorQueueFactory queueFactory) { + this.producer = queueFactory.createJobStatsProducer(); + } public void reportTaskResult(TenantId tenantId, JobId jobId, TaskResult result) { report(tenantId, jobId, JobStatsMsg.newBuilder() @@ -59,7 +60,6 @@ public class JobStatsService { .setJobIdLSB(jobId.getId().getLeastSignificantBits()); TbProtoQueueMsg msg = new TbProtoQueueMsg<>(jobId.getId(), statsMsg.build()); - TbQueueProducer> producer = producerProvider.getJobStatsProducer(); producer.send(TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(), msg, TbQueueCallback.EMPTY); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java new file mode 100644 index 0000000000..77a47a20c8 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java @@ -0,0 +1,82 @@ +/** + * 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.queue.task; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; +import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; +import org.thingsboard.server.queue.kafka.TbKafkaSettings; +import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; + +@Component +@ConditionalOnExpression("'${queue.type:null}'=='kafka'") +public class KafkaTaskProcessorQueueFactory implements TaskProcessorQueueFactory { + + private final TopicService topicService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TbKafkaSettings kafkaSettings; + private final TbKafkaConsumerStatsService consumerStatsService; + + private final TbQueueAdmin tasksAdmin; + + public KafkaTaskProcessorQueueFactory(TopicService topicService, + TbServiceInfoProvider serviceInfoProvider, + TbKafkaSettings kafkaSettings, + TbKafkaConsumerStatsService consumerStatsService, + TbKafkaTopicConfigs kafkaTopicConfigs) { + this.serviceInfoProvider = serviceInfoProvider; + this.kafkaSettings = kafkaSettings; + this.topicService = topicService; + this.consumerStatsService = consumerStatsService; + this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); + } + + @Override + public TbQueueConsumer> createTaskConsumer(JobType jobType) { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(jobType.getTasksTopic())) + .clientId(jobType.name().toLowerCase() + "-task-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName(jobType.name().toLowerCase() + "-task-consumer-group")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TaskProto.parseFrom(msg.getData()), msg.getHeaders())) + .admin(tasksAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createJobStatsProducer() { + return TbKafkaProducerTemplate.>builder() + .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName("jobs.stats")) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 82f13ce66b..319ddf4ab9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -37,7 +37,6 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -import org.thingsboard.server.queue.provider.TaskProcessorQueueFactory; import java.util.List; import java.util.Set; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorQueueFactory.java similarity index 96% rename from common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java rename to common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorQueueFactory.java index 571b14639c..c5e8035d74 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TaskProcessorQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessorQueueFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.queue.provider; +package org.thingsboard.server.queue.task; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java b/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java deleted file mode 100644 index 1f1152af68..0000000000 --- a/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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.edqs; - -import org.springframework.stereotype.Service; -import org.thingsboard.server.queue.discovery.QueueRoutingInfo; -import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; - -import java.util.Collections; -import java.util.List; - -@Service -public class DummyQueueRoutingInfoService implements QueueRoutingInfoService { - - @Override - public List getAllQueuesRoutingInfo() { - return Collections.emptyList(); - } - -} diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java b/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java deleted file mode 100644 index 4e16e5e16a..0000000000 --- a/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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.edqs; - -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.queue.discovery.TenantRoutingInfo; -import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; - -@Service -public class DummyTenantRoutingInfoService implements TenantRoutingInfoService { - @Override - public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { - return null; - } - -} From ac9e738018f272d165a9971213df0251114762b8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 2 May 2025 17:43:14 +0300 Subject: [PATCH 22/44] Task processing timeout --- .../housekeeper/HousekeeperService.java | 1 + .../service/job/task/DummyTaskProcessor.java | 5 ++ .../server/service/job/JobManagerTest.java | 22 +++++++++ .../server/queue/task/TaskProcessor.java | 35 +++++++++++--- common/util/pom.xml | 4 ++ .../org/thingsboard/common/util/SetCache.java | 47 +++++++++++++++++++ 6 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/SetCache.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 727e27c971..f2d3fad357 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -165,6 +165,7 @@ public class HousekeeperService { private void stop() { consumer.stop(); consumerExecutor.shutdownNow(); + taskExecutor.shutdownNow(); log.info("Stopped Housekeeper service"); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 564142ff2b..1bcf6b36b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -39,6 +39,11 @@ public class DummyTaskProcessor extends TaskProcessor { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + JobResult jobResult = job.getResult(); + assertThat(jobResult.getFailedCount()).isEqualTo(1); + assertThat(((DummyTaskResult) jobResult.getResults().get(0)).getFailure().getError()).isEqualTo("Timeout after 2000 ms"); // last error + }); + } + @Test public void testCancelJob_whileRunning() throws Exception { int tasksCount = 100; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 319ddf4ab9..d243ef514d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.SetCache; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; @@ -41,7 +43,12 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public abstract class TaskProcessor, R extends TaskResult> { @@ -56,9 +63,10 @@ public abstract class TaskProcessor, R extends TaskResult> { private QueueKey queueKey; private MainQueueConsumerManager, QueueConfig> taskConsumer; + private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); - private final Set deletedTenants = ConcurrentHashMap.newKeySet(); - private final Set discardedJobs = ConcurrentHashMap.newKeySet(); // fixme use caffeine + private final SetCache discardedJobs = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); + private final SetCache deletedTenants = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); @PostConstruct public void init() { @@ -124,21 +132,34 @@ public abstract class TaskProcessor, R extends TaskResult> { consumer.commit(); } - private void processTask(T task) throws Exception { // todo: timeout and task interruption + private void processTask(T task) throws InterruptedException { task.setAttempt(task.getAttempt() + 1); log.info("Processing task: {}", task); + Future future = null; try { - R result = process(task); + future = taskExecutor.submit(() -> process(task)); + R result; + try { + result = future.get(getTaskProcessingTimeout(), TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + throw e.getCause(); + } catch (TimeoutException e) { + throw new TimeoutException("Timeout after " + getTaskProcessingTimeout() + " ms"); + } reportTaskResult(task, result); } catch (InterruptedException e) { throw e; - } catch (Exception e) { + } catch (Throwable e) { log.error("Failed to process task (attempt {}): {}", task.getAttempt(), task, e); if (task.getAttempt() <= task.getRetries()) { processTask(task); } else { reportTaskFailure(task, e); } + } finally { + if (future != null && !future.isDone()) { + future.cancel(true); + } } } @@ -166,8 +187,10 @@ public abstract class TaskProcessor, R extends TaskResult> { public void destroy() { taskConsumer.stop(); taskConsumer.awaitStop(); + taskExecutor.shutdownNow(); } + public abstract long getTaskProcessingTimeout(); public abstract JobType getJobType(); diff --git a/common/util/pom.xml b/common/util/pom.xml index 6719dc628a..f69cf794e9 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -116,6 +116,10 @@ exp4j ${exp4j.version} + + com.github.ben-manes.caffeine + caffeine + diff --git a/common/util/src/main/java/org/thingsboard/common/util/SetCache.java b/common/util/src/main/java/org/thingsboard/common/util/SetCache.java new file mode 100644 index 0000000000..9676434534 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/SetCache.java @@ -0,0 +1,47 @@ +/** + * 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.common.util; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.concurrent.TimeUnit; + +public class SetCache { + + private static final Object DUMMY_VALUE = Boolean.TRUE; + + private final Cache cache; + + public SetCache(long valueTtlMs) { + this.cache = Caffeine.newBuilder() + .expireAfterWrite(valueTtlMs, TimeUnit.MILLISECONDS) + .build(); + } + + public void add(K key) { + cache.put(key, DUMMY_VALUE); + } + + public boolean contains(K key) { + return cache.asMap().containsKey(key); + } + + public void remove(K key) { + cache.invalidate(key); + } + +} From 5993b8b9638606f794295825908d733df27f0ef0 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 5 May 2025 10:50:39 +0300 Subject: [PATCH 23/44] Minor refactoring --- .../server/common/msg/queue/TbCallback.java | 17 ++++++++++++++++- .../server/queue/task/TaskProcessor.java | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbCallback.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbCallback.java index ee8990d931..777ce3bf94 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbCallback.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbCallback.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.msg.queue; +import com.google.common.util.concurrent.SettableFuture; import org.thingsboard.server.common.data.id.EntityId; import java.util.UUID; @@ -34,7 +35,7 @@ public interface TbCallback { } }; - default UUID getId(){ + default UUID getId() { return EntityId.NULL_UUID; } @@ -42,4 +43,18 @@ public interface TbCallback { void onFailure(Throwable t); + static TbCallback wrap(SettableFuture future) { + return new TbCallback() { + @Override + public void onSuccess() { + future.set(null); + } + + @Override + public void onFailure(Throwable t) { + future.setException(t); + } + }; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index d243ef514d..30a9929dfb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -183,6 +183,15 @@ public abstract class TaskProcessor, R extends TaskResult> { discardedJobs.add(jobId); } + protected V wait(Future future) throws Exception { + try { + return future.get(); // will be interrupted after task processing timeout + } catch (InterruptedException e) { + future.cancel(true); // interrupting the underlying task + throw e; + } + } + @PreDestroy public void destroy() { taskConsumer.stop(); From 1e5b6cc9a9cfe50bae5fb1db30c09bd25f9b151c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 5 May 2025 15:45:56 +0300 Subject: [PATCH 24/44] Remove task.getKey() --- .../thingsboard/server/common/data/job/task/DummyTask.java | 5 ----- .../org/thingsboard/server/common/data/job/task/Task.java | 4 ---- .../org/thingsboard/server/queue/task/TaskProcessor.java | 4 ++-- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java index 39e1306597..d7a37b5175 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java @@ -36,11 +36,6 @@ public class DummyTask extends Task { private List errors; // errors for each attempt private boolean failAlways; - @Override - public Object getKey() { - return number; - } - @Override public DummyTaskResult toFailed(Throwable error) { return DummyTaskResult.failed(this, error); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java index 624c5e90f8..6bf287b54a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.common.data.job.task; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -46,9 +45,6 @@ public abstract class Task { private int attempt = 0; - @JsonIgnore - public abstract Object getKey(); - public abstract R toFailed(Throwable error); public abstract R toDiscarded(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 30a9929dfb..38d3d02b50 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -115,11 +115,11 @@ public abstract class TaskProcessor, R extends TaskResult> { @SuppressWarnings("unchecked") T task = (T) JacksonUtil.fromString(msg.getValue().getValue(), Task.class); if (discardedJobs.contains(task.getJobId().getId())) { - log.info("Skipping task '{}' for cancelled job {}", task.getKey(), task.getJobId()); + log.debug("Skipping task for cancelled job {}: {}", task.getJobId(), task); reportTaskDiscarded(task); continue; } else if (deletedTenants.contains(task.getTenantId().getId())) { - log.info("Skipping task '{}' for deleted tenant {}", task.getKey(), task.getTenantId()); + log.debug("Skipping task for deleted tenant {}: {}", task.getTenantId(), task); continue; } processTask(task); From 26a41b7c32aebbdaf8aa3d95fc8358d112c25dc2 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 5 May 2025 17:41:30 +0300 Subject: [PATCH 25/44] Introduce TbTelemetryService --- .../controller/TelemetryController.java | 23 ++--- .../telemetry/DefaultTbTelemetryService.java | 92 +++++++++++++++++++ .../service/telemetry/TbTelemetryService.java | 43 +++++++++ 3 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 6ca767dc01..d93e973073 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -92,6 +92,7 @@ import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.telemetry.AttributeData; +import org.thingsboard.server.service.telemetry.TbTelemetryService; import org.thingsboard.server.service.telemetry.TsData; import java.util.ArrayList; @@ -155,6 +156,9 @@ public class TelemetryController extends BaseController { @Autowired private AccessValidator accessValidator; + @Autowired + private TbTelemetryService tbTelemetryService; + @Value("${transport.json.max_string_value_length:0}") private int maxStringValueLength; @@ -323,20 +327,11 @@ public class TelemetryController extends BaseController { @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> { - AggregationParams params; - Aggregation agg = Aggregation.valueOf(aggStr); - if (Aggregation.NONE.equals(agg)) { - params = AggregationParams.none(); - } else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) { - params = interval == 0L ? AggregationParams.none() : AggregationParams.milliseconds(agg, interval); - } else { - params = AggregationParams.calendar(agg, intervalType, timeZone); - } - List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, params, limit, orderBy)).collect(Collectors.toList()); - Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); - }); + DeferredResult response = new DeferredResult<>(); + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, + intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), + getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); + return response; } @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java new file mode 100644 index 0000000000..d55f44a027 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java @@ -0,0 +1,92 @@ +/** + * 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.telemetry; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AggregationParams; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.IntervalType; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.service.security.AccessValidator; +import org.thingsboard.server.service.security.ValidationResult; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DefaultTbTelemetryService implements TbTelemetryService { + + private final TimeseriesService tsService; + private final AccessValidator accessValidator; + + @Override + public ListenableFuture> getTimeseries(EntityId entityId, List keys, Long startTs, Long endTs, IntervalType intervalType, + Long interval, String timeZone, Integer limit, Aggregation agg, String orderBy, + Boolean useStrictDataTypes, SecurityUser currentUser) { + SettableFuture> future = SettableFuture.create(); + accessValidator.validate(currentUser, Operation.READ_TELEMETRY, entityId, new FutureCallback<>() { + @Override + public void onSuccess(ValidationResult validationResult) { + try { + AggregationParams params; + if (Aggregation.NONE.equals(agg)) { + params = AggregationParams.none(); + } else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) { + params = interval == 0L ? AggregationParams.none() : AggregationParams.milliseconds(agg, interval); + } else { + params = AggregationParams.calendar(agg, intervalType, timeZone); + } + List queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, params, limit, orderBy)).collect(Collectors.toList()); + Futures.addCallback(tsService.findAll(currentUser.getTenantId(), entityId, queries), new FutureCallback<>() { + @Override + public void onSuccess(List result) { + future.set(result); + } + + @Override + public void onFailure(Throwable t) { + future.setException(t); + } + }, MoreExecutors.directExecutor()); + } catch (Throwable e) { + onFailure(e); + } + } + + @Override + public void onFailure(Throwable t) { + future.setException(t); + } + }); + return future; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java new file mode 100644 index 0000000000..9820e62592 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java @@ -0,0 +1,43 @@ +/** + * 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.telemetry; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.IntervalType; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; + +public interface TbTelemetryService { + + ListenableFuture> getTimeseries(EntityId entityId, + List keys, + Long startTs, + Long endTs, + IntervalType intervalType, + Long interval, + String timeZone, + Integer limit, + Aggregation agg, + String orderBy, + Boolean useStrictDataTypes, + SecurityUser currentUser) throws ThingsboardException; + +} From ac77910d9aeacda66c635015f467f8774c716d64 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 6 May 2025 14:24:51 +0300 Subject: [PATCH 26/44] Fix HashPartitionServiceTest --- .../server/queue/discovery/HashPartitionServiceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index a149ca6dfd..52cc5eb00f 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -436,6 +436,7 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12); + ReflectionTestUtils.setField(partitionService, "tasksPartitions", 12); partitionService.init(); partitionService.partitionsInit(); return partitionService; From f5e816923d756f5faa780d106c2766b7653d1a84 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 8 May 2025 11:55:13 +0300 Subject: [PATCH 27/44] Tasks partitioning strategies; jobs filter; fixes --- .../server/controller/JobController.java | 14 +++++- .../controller/TelemetryController.java | 25 +++++------ .../server/service/job/DefaultJobManager.java | 20 ++++++--- .../src/main/resources/thingsboard.yml | 5 +++ .../server/controller/AbstractWebTest.java | 7 +++ .../server/service/job/JobManagerTest.java | 32 ++++++++------ ...anagerTest_EntityPartitioningStrategy.java | 43 +++++++++++++++++++ .../server/dao/job/JobService.java | 3 +- .../server/common/data/id/JobId.java | 2 +- .../common/data/job/JobConfiguration.java | 2 + .../server/common/data/job/JobFilter.java | 30 +++++++++++++ .../server/common/data/job/JobResult.java | 3 +- .../common/data/job/task/DummyTask.java | 8 ++++ .../server/common/data/job/task/Task.java | 6 +++ .../common/data/job/task/TaskResult.java | 2 + common/proto/src/main/proto/queue.proto | 4 +- .../queue/discovery/HashPartitionService.java | 21 ++++++--- .../server/queue/task/TaskProcessor.java | 2 +- .../server/dao/job/DefaultJobService.java | 5 ++- .../thingsboard/server/dao/job/JobDao.java | 3 +- .../server/dao/sql/job/JobRepository.java | 28 ++++++------ .../server/dao/sql/job/JpaJobDao.java | 12 ++++-- .../resources/sql/schema-entities-idx.sql | 2 + 23 files changed, 211 insertions(+), 68 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index 9b6627e12a..1db84593f5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -28,12 +28,16 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; +import org.thingsboard.server.common.data.job.JobStatus; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.job.JobManager; +import java.util.List; import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -69,10 +73,16 @@ public class JobController extends BaseController { @Parameter(description = SORT_PROPERTY_DESCRIPTION) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) List types, + @RequestParam(required = false) List statuses) throws ThingsboardException { // todo check permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return jobService.findJobsByTenantId(getTenantId(), pageLink); + JobFilter filter = JobFilter.builder() + .types(types) + .statuses(statuses) + .build(); + return jobService.findJobsByFilter(getTenantId(), filter, pageLink); } @PostMapping("/job/{id}/cancel") diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index d93e973073..bf9713f58e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -64,11 +64,9 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.AggregationParams; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; @@ -78,7 +76,6 @@ import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -317,10 +314,10 @@ public class TelemetryController extends BaseController { @Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.") @RequestParam(name = "timeZone", required = false) String timeZone, @Parameter(description = "An integer value that represents a max number of time series data points to fetch." + - " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) + " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) @RequestParam(name = "limit", defaultValue = "100") Integer limit, @Parameter(description = "A string value representing the aggregation function. " + - "If the interval is not specified, 'agg' parameter will use 'NONE' value.", + "If the interval is not specified, 'agg' parameter will use 'NONE' value.", schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"})) @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @@ -340,12 +337,12 @@ public class TelemetryController extends BaseController { + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + - "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + + "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), @ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), @ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) @@ -463,11 +460,11 @@ public class TelemetryController extends BaseController { TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @@ -544,11 +541,11 @@ public class TelemetryController extends BaseController { "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @@ -566,11 +563,11 @@ public class TelemetryController extends BaseController { INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 79312ba608..8c32de4f45 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -24,6 +24,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -79,6 +80,8 @@ public class DefaultJobManager implements JobManager { private final ExecutorService executor; private final ExecutorService consumerExecutor; + @Value("${queue.tasks.partitioning_strategy:tenant}") + private String tasksPartitioningStrategy; @Value("${queue.tasks.stats.processing_interval_ms:1000}") private int statsProcessingInterval; @@ -148,7 +151,7 @@ public class DefaultJobManager implements JobManager { JobProcessor processor = getJobProcessor(job.getType()); List toReprocess = job.getConfiguration().getToReprocess(); if (toReprocess == null) { - int tasksCount = processor.process(job, this::submitTask); // todo: think about stopping tb - while tasks are being submitted + int tasksCount = processor.process(job, this::submitTask); log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount); jobStatsService.reportAllTasksSubmitted(tenantId, jobId, tasksCount); } else { @@ -197,17 +200,24 @@ public class DefaultJobManager implements JobManager { } private void submitTask(Task task) { - log.info("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); + log.debug("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); TaskProto taskProto = TaskProto.newBuilder() .setValue(JacksonUtil.toString(task)) .build(); TbQueueProducer> producer = taskProducers.get(task.getJobType()); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TASK_PROCESSOR, task.getJobType().name(), task.getTenantId(), task.getTenantId()); // one job at a time for a given tenant + EntityId entityId = null; + if (tasksPartitioningStrategy.equals("entity")) { + entityId = task.getEntityId(); + } + if (entityId == null) { + entityId = task.getTenantId(); + } + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TASK_PROCESSOR, task.getJobType().name(), task.getTenantId(), entityId); producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), taskProto), new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.trace("Submitted task: {}", task); + log.trace("Submitted task to {}: {}", tpi, taskProto); } @Override @@ -247,7 +257,7 @@ public class DefaultJobManager implements JobManager { }); consumer.commit(); - Thread.sleep(statsProcessingInterval); // todo: test with bigger interval + Thread.sleep(statsProcessingInterval); } private void sendJobFinishedNotification(Job job) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 54db919bbf..7cae5db294 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1900,6 +1900,11 @@ queue: tasks: # Partitions count for tasks queues partitions: "${TB_QUEUE_TASKS_PARTITIONS:12}" + # Custom partitions count for tasks queues per type. Format: 'TYPE1:24;TYPE2:36', e.g. 'CF_REPROCESSING:24;TENANT_EXPORT:6' + partitions_per_type: "${TB_QUEUE_TASKS_PARTITIONS_PER_TYPE:}" + # Tasks partitioning strategy: 'tenant' or 'entity'. By default, using 'tenant' - tasks of a specific tenant are processed in the same partition. + # In a single-tenant environment, use 'entity' strategy to distribute the tasks among multiple partitions. + partitioning_strategy: "${TB_QUEUE_TASKS_PARTITIONING_STRATEGY:tenant}" stats: # Interval in milliseconds to process job stats processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 054c2fbe59..ef658fc17a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -109,6 +109,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationType; @@ -168,6 +169,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -1270,6 +1272,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } + protected List findJobs(JobType... types) throws Exception { + return doGetTypedWithPageLink("/api/jobs?types=" + Arrays.stream(types).map(Enum::name).collect(Collectors.joining(",")) + "&", + new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); + } + protected void cancelJob(JobId jobId) throws Exception { doPost("/api/job/" + jobId + "/cancel").andExpect(status().isOk()); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 15037470d1..206acc450d 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.job; -import org.assertj.core.api.Assertions; import org.assertj.core.api.ThrowingConsumer; import org.junit.After; import org.junit.Before; @@ -34,13 +33,12 @@ import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.notification.Notification; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.task.JobStatsService; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -61,9 +59,6 @@ public class JobManagerTest extends AbstractControllerTest { @Autowired private JobManager jobManager; - @Autowired - private JobService jobService; - @SpyBean private TestTaskProcessor taskProcessor; @@ -138,8 +133,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks); - assertThat(((DummyTaskResult) jobResult.getResults().get(0)).getFailure().getError()).isEqualTo("error3"); // last error - assertThat(((DummyTaskResult) jobResult.getResults().get(1)).getFailure().getError()).isEqualTo("error3"); // last error + assertThat(getFailures(jobResult)).hasSize(2).allSatisfy(failure -> { + assertThat(failure.getError()).isEqualTo("error3"); // last error + }); assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); }); @@ -254,7 +250,7 @@ public class JobManagerTest extends AbstractControllerTest { Thread.sleep(3000); verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); - Assertions.assertThat(jobService.findJobsByTenantId(tenantId, new PageLink(100, 0)).getData()).isEmpty(); + assertThat(findJobs()).isEmpty(); } @Test @@ -276,7 +272,7 @@ public class JobManagerTest extends AbstractControllerTest { } await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - List jobs = findJobs(); + List jobs = findJobs(JobType.DUMMY); assertThat(jobs).hasSize(jobsCount); Job firstJob = jobs.get(2); // ordered by createdTime descending assertThat(firstJob.getStatus()).isEqualTo(JobStatus.RUNNING); @@ -391,8 +387,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); + List failures = getFailures(jobResult); for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); + DummyTaskFailure failure = failures.get(i); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); } @@ -438,8 +435,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks + permanentlyFailedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + List failures = getFailures(jobResult); for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); + DummyTaskFailure failure = failures.get(i); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); } @@ -455,8 +453,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getFailedCount()).isEqualTo(permanentlyFailedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + List failures = getFailures(jobResult); for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { - DummyTaskFailure failure = ((DummyTaskResult) jobResult.getResults().get(i)).getFailure(); + DummyTaskFailure failure = failures.get(i); assertThat(failure.getNumber()).isEqualTo(taskNumber); assertThat(failure.getError()).isEqualTo("error"); assertThat(failure.isFailAlways()).isTrue(); @@ -474,6 +473,11 @@ public class JobManagerTest extends AbstractControllerTest { }); } - // todo: job with zero tasks + private List getFailures(JobResult jobResult) { + return jobResult.getResults().stream() + .map(taskResult -> ((DummyTaskResult) taskResult).getFailure()) + .sorted(Comparator.comparingInt(DummyTaskFailure::getNumber)) + .toList(); + } } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java new file mode 100644 index 0000000000..983f30d523 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java @@ -0,0 +1,43 @@ +/** + * 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.job; + +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +@TestPropertySource(properties = { + "queue.tasks.stats.processing_interval_ms=0", + "queue.tasks.partitioning_strategy=entity", + "queue.tasks.partitions_per_type=DUMMY:100;DUMMY:50" +}) +public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest { + + /* + * Some tests are overridden because they are based on + * tenant partitioning strategy (subsequent tasks processing within a tenant) + * */ + + @Override + public void testCancelJob_simulateTaskProcessorRestart() throws Exception { + } + + @Override + public void testGeneralJobError() { + + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 3c00b2fe0e..3204044880 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.job; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -35,7 +36,7 @@ public interface JobService extends EntityDaoService { void processStats(TenantId tenantId, JobId jobId, JobStats jobStats); - PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findJobsByFilter(TenantId tenantId, JobFilter filter, PageLink pageLink); Job findLatestJobByKey(TenantId tenantId, String key); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index e6688f0eb0..76678b8b31 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -29,7 +29,7 @@ public class JobId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TASK", allowableValues = "TASK") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB") @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 8aed4adbe5..35f44cd4be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -35,6 +36,7 @@ public abstract class JobConfiguration implements Serializable { private List toReprocess; + @JsonIgnore public abstract JobType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java new file mode 100644 index 0000000000..6cc9a636e8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java @@ -0,0 +1,30 @@ +/** + * 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.common.data.job; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class JobFilter { + + private final List types; + private final List statuses; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 534e3587bb..3af076cd19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -58,7 +58,7 @@ public abstract class JobResult implements Serializable { discardedCount++; } else { failedCount++; - if (results.size() < 1000) { // preserving only first 1000 errors, not reprocessing if there are more failures + if (results.size() < 100) { // preserving only first 100 errors, not reprocessing if there are more failures results.add(taskResult); } } @@ -67,6 +67,7 @@ public abstract class JobResult implements Serializable { @JsonIgnore public abstract String getDescription(); + @JsonIgnore public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java index d7a37b5175..7e262ed7b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java @@ -20,9 +20,12 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.job.JobType; import java.util.List; +import java.util.UUID; @Data @NoArgsConstructor @@ -46,6 +49,11 @@ public class DummyTask extends Task { return DummyTaskResult.discarded(); } + @Override + public EntityId getEntityId() { + return new DeviceId(UUID.randomUUID()); + } + @Override public JobType getJobType() { return JobType.DUMMY; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java index 6bf287b54a..cce32fdad0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job.task; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -22,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.JobType; @@ -49,6 +51,10 @@ public abstract class Task { public abstract R toDiscarded(); + @JsonIgnore + public abstract EntityId getEntityId(); + + @JsonIgnore public abstract JobType getJobType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 8c4667e1be..9416cb8f6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job.task; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -39,6 +40,7 @@ public abstract class TaskResult { private boolean success; private boolean discarded; + @JsonIgnore public abstract JobType getJobType(); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 1cc507c5b3..04c41a7f69 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1854,7 +1854,7 @@ message EdqsResponseMsg { } message TaskProto { - string value = 1; // fixme: TMP, make more efficient + string value = 1; } message JobStatsMsg { @@ -1867,5 +1867,5 @@ message JobStatsMsg { } message TaskResultProto { - string value = 1; // fixme: TMP, make more efficient + string value = 1; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 3f81199488..d6580d5c83 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -40,12 +40,14 @@ import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.PropertyUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -87,7 +89,9 @@ public class HashPartitionService implements PartitionService { @Value("${queue.edqs.partitions:12}") private Integer edqsPartitions; @Value("${queue.tasks.partitions:12}") - private Integer tasksPartitions; + private Integer defaultTasksPartitions; + @Value("${queue.tasks.partitions_per_type:}") + private String tasksPartitionsPerType; @Value("${queue.partitions.hash_function_name:murmur3_128}") private String hashFunctionName; @@ -135,11 +139,18 @@ public class HashPartitionService implements PartitionService { partitionSizesMap.put(edqsKey, edqsPartitions); partitionTopicsMap.put(edqsKey, "edqs"); // placeholder, not used - for (JobType jobType : JobType.values()) { - QueueKey queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, jobType.name()); - partitionSizesMap.put(queueKey, tasksPartitions); - partitionTopicsMap.put(queueKey, jobType.getTasksTopic()); + Map tasksPartitions = new EnumMap<>(JobType.class); + PropertyUtils.getProps(tasksPartitionsPerType).forEach((type, partitions) -> { + tasksPartitions.put(JobType.valueOf(type), Integer.parseInt(partitions)); + }); + for (JobType type : JobType.values()) { + tasksPartitions.putIfAbsent(type, defaultTasksPartitions); } + tasksPartitions.forEach((type, partitions) -> { + QueueKey queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, type.name()); + partitionSizesMap.put(queueKey, partitions); + partitionTopicsMap.put(queueKey, type.getTasksTopic()); + }); } @AfterStartUp(order = AfterStartUp.QUEUE_INFO_INITIALIZATION) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 38d3d02b50..fd4367781e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -63,7 +63,7 @@ public abstract class TaskProcessor, R extends TaskResult> { private QueueKey queueKey; private MainQueueConsumerManager, QueueConfig> taskConsumer; - private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); + private final ExecutorService taskExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); private final SetCache discardedJobs = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); private final SetCache deletedTenants = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 73e0c5dff4..680bc81566 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; @@ -174,8 +175,8 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } @Override - public PageData findJobsByTenantId(TenantId tenantId, PageLink pageLink) { - return jobDao.findByTenantId(tenantId, pageLink); + public PageData findJobsByFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { + return jobDao.findByTenantIdAndFilter(tenantId, filter, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index afe182d8cd..0c70fd102d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.job; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.Dao; public interface JobDao extends Dao { - PageData findByTenantId(TenantId tenantId, PageLink pageLink); + PageData findByTenantIdAndFilter(TenantId tenantId, JobFilter filter, PageLink pageLink); Job findByIdForUpdate(TenantId tenantId, JobId jobId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index 0ecd517f51..72d569c94b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -15,12 +15,9 @@ */ package org.thingsboard.server.dao.sql.job; -import jakarta.persistence.LockModeType; -import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -37,28 +34,29 @@ import java.util.UUID; public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId " + - "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + - "OR ilike(j.description, concat('%', :searchText, '%')) = true)") - Page findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId, - @Param("searchText") String searchText, - Pageable pageable); + "AND (:types IS NULL OR j.type IN (:types)) AND (:statuses IS NULL OR j.status IN (:statuses)) " + + "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + + "OR ilike(j.description, concat('%', :searchText, '%')) = true)") + Page findByTenantIdAndTypesAndStatusesAndSearchText(@Param("tenantId") UUID tenantId, + @Param("types") List types, + @Param("statuses") List statuses, + @Param("searchText") String searchText, + Pageable pageable); - @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE - @Query("SELECT j FROM JobEntity j WHERE j.id = :id") + @Query(value = "SELECT * FROM job j WHERE j.id = :id FOR UPDATE", nativeQuery = true) JobEntity findByIdForUpdate(UUID id); @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.key = :key " + - "ORDER BY j.createdTime DESC") + "ORDER BY j.createdTime DESC") JobEntity findLatestByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key); boolean existsByTenantIdAndKeyAndStatusIn(UUID tenantId, String key, List statuses); boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); - @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE - @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.type = :type " + - "AND j.status = :status ORDER BY j.createdTime ASC, j.id ASC") - JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, JobType type, JobStatus status, Limit limit); + @Query(value = "SELECT * FROM job j WHERE j.tenant_id = :tenantId AND j.type = :type " + + "AND j.status = :status ORDER BY j.created_time ASC, j.id ASC LIMIT 1 FOR UPDATE", nativeQuery = true) + JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, String type, String status); @Transactional @Modifying diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 0e5ee4683e..f59898eb73 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -17,17 +17,18 @@ package org.thingsboard.server.dao.sql.job; import com.google.common.base.Strings; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.model.sql.JobEntity; @@ -45,8 +46,11 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao private final JobRepository jobRepository; @Override - public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(jobRepository.findByTenantIdAndSearchText(tenantId.getId(), Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); + public PageData findByTenantIdAndFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { + return DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndSearchText(tenantId.getId(), + CollectionsUtil.isEmpty(filter.getTypes()) ? null : filter.getTypes(), + CollectionsUtil.isEmpty(filter.getStatuses()) ? null : filter.getStatuses(), + Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); } @Override @@ -71,7 +75,7 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao @Override public Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status) { - return DaoUtil.getData(jobRepository.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId.getId(), type, status, Limit.of(1))); + return DaoUtil.getData(jobRepository.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId.getId(), type.name(), status.name())); } @Override diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 7f52365e33..ad311f00df 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -129,3 +129,5 @@ CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key); CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id); From 8c959232d48e00caf5a7e4ad488b3de4ebbe147f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 8 May 2025 16:49:33 +0300 Subject: [PATCH 28/44] Configurable tasks poll interval; task processing timing --- application/src/main/resources/thingsboard.yml | 2 ++ .../server/queue/task/TaskProcessor.java | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7cae5db294..9d14afb541 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1898,6 +1898,8 @@ queue: # Statistics printing interval for Edge services print-interval-ms: "${TB_QUEUE_EDGE_STATS_PRINT_INTERVAL_MS:60000}" tasks: + # Poll interval in milliseconds for tasks topics + poll_interval: "${TB_QUEUE_TASKS_POLL_INTERVAL_MS:500}" # Partitions count for tasks queues partitions: "${TB_QUEUE_TASKS_PARTITIONS:12}" # Custom partitions count for tasks queues per type. Format: 'TYPE1:24;TYPE2:36', e.g. 'CF_REPROCESSING:24;TENANT_EXPORT:6' diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index fd4367781e..ec113c03a1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -20,6 +20,7 @@ import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.SetCache; @@ -61,6 +62,9 @@ public abstract class TaskProcessor, R extends TaskResult> { @Autowired private TaskProcessorExecutors executors; + @Value("${queue.tasks.poll_interval:500}") + private int pollInterval; + private QueueKey queueKey; private MainQueueConsumerManager, QueueConfig> taskConsumer; private final ExecutorService taskExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); @@ -73,7 +77,7 @@ public abstract class TaskProcessor, R extends TaskResult> { queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, getJobType().name()); taskConsumer = MainQueueConsumerManager., QueueConfig>builder() .queueKey(queueKey) - .config(QueueConfig.of(true, 500)) + .config(QueueConfig.of(true, pollInterval)) .msgPackProcessor(this::processMsgs) .consumerCreator((queueConfig, tpi) -> queueFactory.createTaskConsumer(getJobType())) .consumerExecutor(executors.getConsumersExecutor()) @@ -96,14 +100,14 @@ public abstract class TaskProcessor, R extends TaskResult> { switch (entityId.getEntityType()) { case JOB -> { if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { - log.debug("Adding job {} to discarded", entityId); + log.info("Adding job {} to discarded", entityId); addToDiscardedJobs(entityId.getId()); } } case TENANT -> { if (event.getEvent() == ComponentLifecycleEvent.DELETED) { deletedTenants.add(entityId.getId()); - log.debug("Adding tenant {} to deleted", entityId); + log.info("Adding tenant {} to deleted", entityId); } } } @@ -134,9 +138,10 @@ public abstract class TaskProcessor, R extends TaskResult> { private void processTask(T task) throws InterruptedException { task.setAttempt(task.getAttempt() + 1); - log.info("Processing task: {}", task); + log.debug("Processing task: {}", task); Future future = null; try { + long startNs = System.nanoTime(); future = taskExecutor.submit(() -> process(task)); R result; try { @@ -146,6 +151,8 @@ public abstract class TaskProcessor, R extends TaskResult> { } catch (TimeoutException e) { throw new TimeoutException("Timeout after " + getTaskProcessingTimeout() + " ms"); } + long timingNs = System.nanoTime() - startNs; + log.info("Processed task in {} ms: {}", timingNs / 1000000.0, task); reportTaskResult(task, result); } catch (InterruptedException e) { throw e; From faf4a2165f7520cf8ea9715bb04479df89096a6c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 12 May 2025 17:17:57 +0300 Subject: [PATCH 29/44] Fix HashPartitionServiceTest --- .../server/queue/discovery/HashPartitionServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 52cc5eb00f..e21457f65d 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -436,7 +436,7 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12); - ReflectionTestUtils.setField(partitionService, "tasksPartitions", 12); + ReflectionTestUtils.setField(partitionService, "defaultTasksPartitions", 12); partitionService.init(); partitionService.partitionsInit(); return partitionService; From 7d0b6bfdec480d00784890d0b517b4350758c51a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 13 May 2025 11:54:36 +0300 Subject: [PATCH 30/44] Refactoring for tasks api --- .../src/main/resources/thingsboard.yml | 1 + .../server/queue/TbQueueCallback.java | 2 +- .../InMemoryMonolithQueueFactory.java | 4 ++- .../provider/KafkaMonolithQueueFactory.java | 8 +++-- .../provider/KafkaTbCoreQueueFactory.java | 6 +++- .../queue/settings/TasksQueueConfig.java | 32 +++++++++++++++++++ .../InMemoryTaskProcessorQueueFactory.java | 4 ++- .../task/KafkaTaskProcessorQueueFactory.java | 8 +++-- .../server/queue/task/TaskProcessor.java | 9 +++--- .../server/dao/job/DefaultJobService.java | 2 +- .../thingsboard/server/dao/job/JobDao.java | 2 +- .../server/dao/model/sql/JobEntity.java | 3 -- .../server/dao/sql/job/JpaJobDao.java | 2 +- .../main/resources/sql/schema-entities.sql | 4 +-- 14 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9d14afb541..4211b4e845 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1908,6 +1908,7 @@ queue: # In a single-tenant environment, use 'entity' strategy to distribute the tasks among multiple partitions. partitioning_strategy: "${TB_QUEUE_TASKS_PARTITIONING_STRATEGY:tenant}" stats: + topic: "${TB_QUEUE_TASKS_STATS_TOPIC:jobs.stats}" # Interval in milliseconds to process job stats processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueCallback.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueCallback.java index e15d9c8ace..99523bf21f 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueCallback.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueCallback.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.queue; - public interface TbQueueCallback { TbQueueCallback EMPTY = new TbQueueCallback() { @@ -34,4 +33,5 @@ public interface TbQueueCallback { void onSuccess(TbQueueMsgMetadata metadata); void onFailure(Throwable t); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index a164237366..ddaf34715f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -43,6 +43,7 @@ import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; +import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; @@ -68,6 +69,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueEdgeSettings edgeSettings; private final TbQueueCalculatedFieldSettings calculatedFieldSettings; private final EdqsConfig edqsConfig; + private final TasksQueueConfig tasksQueueConfig; private final InMemoryStorage storage; @Override @@ -267,7 +269,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createJobStatsConsumer() { - return new InMemoryTbQueueConsumer<>(storage, "jobs.stats"); + return new InMemoryTbQueueConsumer<>(storage, tasksQueueConfig.getStatsTopic()); } @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index b15e60f09d..902848f65f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -66,6 +66,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; @@ -95,6 +96,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueCalculatedFieldSettings calculatedFieldSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final EdqsConfig edqsConfig; + private final TasksQueueConfig tasksQueueConfig; private final TbQueueAdmin coreAdmin; private final TbKafkaAdmin ruleEngineAdmin; @@ -130,7 +132,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueCalculatedFieldSettings calculatedFieldSettings, TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs, - EdqsConfig edqsConfig) { + EdqsConfig edqsConfig, + TasksQueueConfig tasksQueueConfig) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -144,6 +147,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.edgeSettings = edgeSettings; this.calculatedFieldSettings = calculatedFieldSettings; this.edqsConfig = edqsConfig; + this.tasksQueueConfig = tasksQueueConfig; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -660,7 +664,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName("jobs.stats")) + .topic(topicService.buildTopicName(tasksQueueConfig.getStatsTopic())) .clientId("job-stats-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("job-stats-consumer-group")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), JobStatsMsg.parseFrom(msg.getData()), msg.getHeaders())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 0bf6acd830..048b08f15a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -62,6 +62,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; @@ -91,6 +92,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueEdgeSettings edgeSettings; private final TbQueueCalculatedFieldSettings calculatedFieldSettings; private final EdqsConfig edqsConfig; + private final TasksQueueConfig tasksQueueConfig; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -126,6 +128,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueCalculatedFieldSettings calculatedFieldSettings, EdqsConfig edqsConfig, + TasksQueueConfig tasksQueueConfig, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; @@ -140,6 +143,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.edgeSettings = edgeSettings; this.calculatedFieldSettings = calculatedFieldSettings; this.edqsConfig = edqsConfig; + this.tasksQueueConfig = tasksQueueConfig; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -539,7 +543,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName("jobs.stats")) + .topic(topicService.buildTopicName(tasksQueueConfig.getStatsTopic())) .clientId("job-stats-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("job-stats-consumer-group")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), JobStatsMsg.parseFrom(msg.getData()), msg.getHeaders())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java new file mode 100644 index 0000000000..f4916a411e --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java @@ -0,0 +1,32 @@ +/** + * 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.queue.settings; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Data +@Component +public class TasksQueueConfig { + + @Value("${queue.tasks.poll_interval}") + private int pollInterval; + + @Value("${queue.tasks.stats.topic}") + private String statsTopic; + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java index dbe302dfee..76effa6304 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProcessorQueueFactory.java @@ -27,6 +27,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; +import org.thingsboard.server.queue.settings.TasksQueueConfig; @Component @ConditionalOnExpression("'${queue.type:null}'=='in-memory'") @@ -34,6 +35,7 @@ import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; public class InMemoryTaskProcessorQueueFactory implements TaskProcessorQueueFactory { private final InMemoryStorage storage; + private final TasksQueueConfig tasksQueueConfig; @Override public TbQueueConsumer> createTaskConsumer(JobType jobType) { @@ -42,7 +44,7 @@ public class InMemoryTaskProcessorQueueFactory implements TaskProcessorQueueFact @Override public TbQueueProducer> createJobStatsProducer() { - return new InMemoryTbQueueProducer<>(storage, "jobs.stats"); + return new InMemoryTbQueueProducer<>(storage, tasksQueueConfig.getStatsTopic()); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java index 77a47a20c8..6b95b0530c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProcessorQueueFactory.java @@ -32,6 +32,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.settings.TasksQueueConfig; @Component @ConditionalOnExpression("'${queue.type:null}'=='kafka'") @@ -39,6 +40,7 @@ public class KafkaTaskProcessorQueueFactory implements TaskProcessorQueueFactory private final TopicService topicService; private final TbServiceInfoProvider serviceInfoProvider; + private final TasksQueueConfig tasksQueueConfig; private final TbKafkaSettings kafkaSettings; private final TbKafkaConsumerStatsService consumerStatsService; @@ -46,12 +48,14 @@ public class KafkaTaskProcessorQueueFactory implements TaskProcessorQueueFactory public KafkaTaskProcessorQueueFactory(TopicService topicService, TbServiceInfoProvider serviceInfoProvider, + TasksQueueConfig tasksQueueConfig, TbKafkaSettings kafkaSettings, TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { + this.topicService = topicService; this.serviceInfoProvider = serviceInfoProvider; + this.tasksQueueConfig = tasksQueueConfig; this.kafkaSettings = kafkaSettings; - this.topicService = topicService; this.consumerStatsService = consumerStatsService; this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); } @@ -73,7 +77,7 @@ public class KafkaTaskProcessorQueueFactory implements TaskProcessorQueueFactory public TbQueueProducer> createJobStatsProducer() { return TbKafkaProducerTemplate.>builder() .clientId("job-stats-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName("jobs.stats")) + .defaultTopic(topicService.buildTopicName(tasksQueueConfig.getStatsTopic())) .settings(kafkaSettings) .admin(tasksAdmin) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index ec113c03a1..947d252442 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -20,7 +20,6 @@ import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.SetCache; @@ -40,6 +39,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.settings.TasksQueueConfig; import java.util.List; import java.util.Set; @@ -61,9 +61,8 @@ public abstract class TaskProcessor, R extends TaskResult> { private JobStatsService statsService; @Autowired private TaskProcessorExecutors executors; - - @Value("${queue.tasks.poll_interval:500}") - private int pollInterval; + @Autowired + private TasksQueueConfig config; private QueueKey queueKey; private MainQueueConsumerManager, QueueConfig> taskConsumer; @@ -77,7 +76,7 @@ public abstract class TaskProcessor, R extends TaskResult> { queueKey = new QueueKey(ServiceType.TASK_PROCESSOR, getJobType().name()); taskConsumer = MainQueueConsumerManager., QueueConfig>builder() .queueKey(queueKey) - .config(QueueConfig.of(true, pollInterval)) + .config(QueueConfig.of(true, config.getPollInterval())) .msgPackProcessor(this::processMsgs) .consumerCreator((queueConfig, tpi) -> queueFactory.createTaskConsumer(getJobType())) .consumerExecutor(executors.getConsumersExecutor()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 680bc81566..ba01b10326 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -181,7 +181,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public Job findLatestJobByKey(TenantId tenantId, String key) { - return jobDao.findLatestByKey(tenantId, key); + return jobDao.findLatestByTenantIdAndKey(tenantId, key); } private Job findForUpdate(TenantId tenantId, JobId jobId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index 0c70fd102d..46cc70aea8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -31,7 +31,7 @@ public interface JobDao extends Dao { Job findByIdForUpdate(TenantId tenantId, JobId jobId); - Job findLatestByKey(TenantId tenantId, String key); + Job findLatestByTenantIdAndKey(TenantId tenantId, String key); boolean existsByTenantAndKeyAndStatusOneOf(TenantId tenantId, String key, JobStatus... statuses); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java index d13c4bbed3..498541962b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java @@ -25,8 +25,6 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import org.hibernate.annotations.JdbcType; -import org.hibernate.dialect.PostgreSQLJsonPGObjectJsonbType; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobConfiguration; @@ -68,7 +66,6 @@ public class JobEntity extends BaseSqlEntity { private JsonNode configuration; @Convert(converter = JsonConverter.class) - @JdbcType(PostgreSQLJsonPGObjectJsonbType.class) @Column(name = ModelConstants.JOB_RESULT_PROPERTY) private JsonNode result; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index f59898eb73..5c9fe230e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -59,7 +59,7 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao } @Override - public Job findLatestByKey(TenantId tenantId, String key) { + public Job findLatestByTenantIdAndKey(TenantId tenantId, String key) { return DaoUtil.getData(jobRepository.findLatestByTenantIdAndKey(tenantId.getId(), key)); } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index ba43a54697..62ce86a76c 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -957,6 +957,6 @@ CREATE TABLE IF NOT EXISTS job ( key varchar NOT NULL, description varchar NOT NULL, status varchar NOT NULL, - configuration varchar(1000000) NOT NULL, - result jsonb + configuration varchar NOT NULL, + result varchar ); From 4fbb6c2e71a080f989900eebf894400caf22fb3e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 13 May 2025 14:48:46 +0300 Subject: [PATCH 31/44] Jobs: refactoring --- .../server/service/job/DefaultJobManager.java | 22 +++++++++---------- .../src/main/resources/thingsboard.yml | 5 ++++- .../server/service/job/JobManagerTest.java | 4 ++-- ...anagerTest_EntityPartitioningStrategy.java | 2 +- .../server/dao/job/JobService.java | 2 +- .../server/common/data/job/Job.java | 2 ++ .../server/common/data/job/JobResult.java | 2 ++ .../server/common/data/job/JobStats.java | 2 ++ common/proto/src/main/proto/queue.proto | 4 ---- .../common/consumer/QueueConsumerManager.java | 14 +++++++++++- .../queue/settings/TasksQueueConfig.java | 17 ++++++++++---- .../server/queue/task/JobStatsService.java | 3 ++- .../server/dao/job/DefaultJobService.java | 4 +++- 13 files changed, 55 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 8c32de4f45..580dd0ff99 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -51,6 +50,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -74,23 +74,21 @@ public class DefaultJobManager implements JobManager { private final JobStatsService jobStatsService; private final NotificationCenter notificationCenter; private final PartitionService partitionService; + private final TasksQueueConfig queueConfig; private final Map jobProcessors; private final Map>> taskProducers; private final QueueConsumerManager> jobStatsConsumer; private final ExecutorService executor; private final ExecutorService consumerExecutor; - @Value("${queue.tasks.partitioning_strategy:tenant}") - private String tasksPartitioningStrategy; - @Value("${queue.tasks.stats.processing_interval_ms:1000}") - private int statsProcessingInterval; - public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, NotificationCenter notificationCenter, - PartitionService partitionService, TbCoreQueueFactory queueFactory, List jobProcessors) { + PartitionService partitionService, TbCoreQueueFactory queueFactory, TasksQueueConfig queueConfig, + List jobProcessors) { this.jobService = jobService; this.jobStatsService = jobStatsService; this.notificationCenter = notificationCenter; this.partitionService = partitionService; + this.queueConfig = queueConfig; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); @@ -98,7 +96,7 @@ public class DefaultJobManager implements JobManager { this.jobStatsConsumer = QueueConsumerManager.>builder() .name("job-stats") .msgPackProcessor(this::processStats) - .pollInterval(125) + .pollInterval(queueConfig.getStatsPollInterval()) .consumerCreator(queueFactory::createJobStatsConsumer) .consumerExecutor(consumerExecutor) .build(); @@ -113,7 +111,7 @@ public class DefaultJobManager implements JobManager { @Override public Job submitJob(Job job) { log.debug("Submitting job: {}", job); - return jobService.submitJob(job.getTenantId(), job); + return jobService.saveJob(job.getTenantId(), job); } @Override @@ -196,7 +194,7 @@ public class DefaultJobManager implements JobManager { job.getConfiguration().setToReprocess(taskFailures); - jobService.submitJob(tenantId, job); + jobService.saveJob(tenantId, job); } private void submitTask(Task task) { @@ -207,7 +205,7 @@ public class DefaultJobManager implements JobManager { TbQueueProducer> producer = taskProducers.get(task.getJobType()); EntityId entityId = null; - if (tasksPartitioningStrategy.equals("entity")) { + if (queueConfig.getPartitioningStrategy().equals("entity")) { entityId = task.getEntityId(); } if (entityId == null) { @@ -257,7 +255,7 @@ public class DefaultJobManager implements JobManager { }); consumer.commit(); - Thread.sleep(statsProcessingInterval); + Thread.sleep(queueConfig.getStatsProcessingInterval()); } private void sendJobFinishedNotification(Job job) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4211b4e845..de641e39b2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1908,9 +1908,12 @@ queue: # In a single-tenant environment, use 'entity' strategy to distribute the tasks among multiple partitions. partitioning_strategy: "${TB_QUEUE_TASKS_PARTITIONING_STRATEGY:tenant}" stats: + # Name for the tasks stats topic topic: "${TB_QUEUE_TASKS_STATS_TOPIC:jobs.stats}" + # Poll interval in milliseconds for tasks stats topic + poll_interval: "${TB_QUEUE_TASKS_STATS_POLL_INTERVAL_MS:500}" # Interval in milliseconds to process job stats - processing_interval_ms: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" + processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" # Event configuration parameters event: diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 206acc450d..85b963a196 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -52,7 +52,7 @@ import static org.mockito.Mockito.verify; @DaoSqlTest @TestPropertySource(properties = { - "queue.tasks.stats.processing_interval_ms=0" + "queue.tasks.stats.processing_interval=0" }) public class JobManagerTest extends AbstractControllerTest { @@ -203,7 +203,7 @@ public class JobManagerTest extends AbstractControllerTest { .description("test job") .configuration(DummyJobConfiguration.builder() .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(100) + .taskProcessingTimeMs(500) .build()) .build()).getId(); diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java index 983f30d523..a021603ca6 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java @@ -20,7 +20,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest @TestPropertySource(properties = { - "queue.tasks.stats.processing_interval_ms=0", + "queue.tasks.stats.processing_interval=0", "queue.tasks.partitioning_strategy=entity", "queue.tasks.partitions_per_type=DUMMY:100;DUMMY:50" }) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 3204044880..33f9511267 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService; public interface JobService extends EntityDaoService { - Job submitJob(TenantId tenantId, Job job); + Job saveJob(TenantId tenantId, Job job); Job findJobById(TenantId tenantId, JobId jobId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index 237c223a92..d4e69761c8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -43,6 +44,7 @@ public class Job extends BaseData implements HasTenantId { private String description; private JobStatus status; @NotNull + @Valid private JobConfiguration configuration; private JobResult result; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 3af076cd19..285143dfa4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -44,6 +44,8 @@ public abstract class JobResult implements Serializable { private List results = new ArrayList<>(); private String generalError; + private long startTs; + private long finishTs; private long cancellationTs; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java index dc3e265f2d..50a3b1d759 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobStats.java @@ -25,8 +25,10 @@ import java.util.List; @Data public class JobStats { + private final TenantId tenantId; private final JobId jobId; private final List taskResults = new ArrayList<>(); private Integer totalTasksCount; + } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 04c41a7f69..2a7d28241e 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -536,10 +536,6 @@ message ToEdqsCoreServiceMsg { bytes value = 1; } -message ToJobManagerMsg { - bytes value = 1; -} - message LwM2MRegistrationRequestMsg { string tenantId = 1; string endpoint = 2; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java index ffed499d8d..5025d887cb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java @@ -25,7 +25,11 @@ import org.thingsboard.server.queue.TbQueueMsg; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; @Slf4j @@ -39,6 +43,7 @@ public class QueueConsumerManager { @Getter private final TbQueueConsumer consumer; + private Future consumerTask; private volatile boolean stopped; @Builder @@ -63,7 +68,7 @@ public class QueueConsumerManager { public void launch() { log.info("[{}] Launching consumer", name); - consumerExecutor.submit(() -> { + consumerTask = consumerExecutor.submit(() -> { if (threadPrefix != null) { ThingsBoardThreadFactory.addThreadNamePrefix(threadPrefix); } @@ -101,6 +106,13 @@ public class QueueConsumerManager { log.debug("[{}] Stopping consumer", name); stopped = true; consumer.unsubscribe(); + try { + if (consumerTask != null) { + consumerTask.get(10, TimeUnit.SECONDS); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + log.error("[{}] Failed to await consumer loop stop", name, e); + } } public interface MsgPackProcessor { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java index f4916a411e..7c94596139 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TasksQueueConfig.java @@ -15,18 +15,27 @@ */ package org.thingsboard.server.queue.settings; -import lombok.Data; +import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -@Data +@Getter @Component public class TasksQueueConfig { - @Value("${queue.tasks.poll_interval}") + @Value("${queue.tasks.poll_interval:500}") private int pollInterval; - @Value("${queue.tasks.stats.topic}") + @Value("${queue.tasks.partitioning_strategy:tenant}") + private String partitioningStrategy; + + @Value("${queue.tasks.stats.topic:jobs.stats}") private String statsTopic; + @Value("${queue.tasks.stats.poll_interval:500}") + private int statsPollInterval; + + @Value("${queue.tasks.stats.processing_interval:1000}") + private int statsProcessingInterval; + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java index 28d08f593c..0b7d18fde4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/JobStatsService.java @@ -53,12 +53,13 @@ public class JobStatsService { } private void report(TenantId tenantId, JobId jobId, JobStatsMsg.Builder statsMsg) { - log.debug("[{}] Reporting: {}", jobId, statsMsg); + log.debug("[{}][{}] Reporting: {}", tenantId, jobId, statsMsg); statsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setJobIdMSB(jobId.getId().getMostSignificantBits()) .setJobIdLSB(jobId.getId().getLeastSignificantBits()); + // using job id as msg key so that all stats for a certain job are submitted to the same partition TbProtoQueueMsg msg = new TbProtoQueueMsg<>(jobId.getId(), statsMsg.build()); producer.send(TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(), msg, TbQueueCallback.EMPTY); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index ba01b10326..50b3e4bde8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -54,7 +54,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Transactional @Override - public Job submitJob(TenantId tenantId, Job job) { + public Job saveJob(TenantId tenantId, Job job) { if (jobDao.existsByTenantAndKeyAndStatusOneOf(tenantId, job.getKey(), QUEUED, PENDING, RUNNING)) { throw new IllegalArgumentException("The same job is already queued or running"); } @@ -62,6 +62,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(QUEUED); } else { job.setStatus(PENDING); + job.getResult().setStartTs(System.currentTimeMillis()); } return saveJob(tenantId, job, true, null); } @@ -140,6 +141,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(COMPLETED); publishEvent = true; } + result.setFinishTs(System.currentTimeMillis()); } } From 55da7ac2b6c6a9c5695d4e2ac3d5822fda5863a7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 14 May 2025 09:49:27 +0300 Subject: [PATCH 32/44] Fix rule engine startup --- .../org/thingsboard/server/service/edqs/EdqsSyncService.java | 2 +- .../server/service/entitiy/EntityStateSourcingListener.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index 871e1fa5e7..88f344d048 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -95,7 +95,7 @@ public abstract class EdqsSyncService { syncLatestTimeseries(); counters.clear(); - log.info("Finishing synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs)); + log.info("Finished synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs)); } private void process(TenantId tenantId, ObjectType type, EdqsObject object) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index f56cc3e952..bcb77e7005 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -62,6 +62,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.service.job.JobManager; +import java.util.Optional; import java.util.Set; @Slf4j @@ -72,7 +73,7 @@ public class EntityStateSourcingListener { private final TenantService tenantService; private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; - private final JobManager jobManager; + private final Optional jobManager; @PostConstruct public void init() { @@ -303,7 +304,7 @@ public class EntityStateSourcingListener { } private void onJobUpdate(Job job) { - jobManager.onJobUpdate(job); + jobManager.ifPresent(jobManager -> jobManager.onJobUpdate(job)); if (job.getResult().getCancellationTs() > 0 || (job.getStatus().isOneOf(JobStatus.FAILED) && job.getResult().getGeneralError() != null)) { // task processors will add this job to the list of discarded tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); From 8dda445253f744a026ad1d89c1ffaa5eccc6be99 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 15 May 2025 11:11:40 +0300 Subject: [PATCH 33/44] Fix test whenTenantIsDeleted_thenCancelAllTheJobs --- .../thingsboard/server/service/job/JobManagerTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 85b963a196..a6e40333eb 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -27,13 +27,16 @@ import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobFilter; import org.thingsboard.server.common.data.job.JobResult; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.task.JobStatsService; @@ -65,6 +68,9 @@ public class JobManagerTest extends AbstractControllerTest { @SpyBean private JobStatsService jobStatsService; + @Autowired + private JobService jobService; + @Before public void setUp() throws Exception { loginTenantAdmin(); @@ -250,7 +256,7 @@ public class JobManagerTest extends AbstractControllerTest { Thread.sleep(3000); verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); - assertThat(findJobs()).isEmpty(); + assertThat(jobService.findJobsByFilter(tenantId, JobFilter.builder().build(), new PageLink(100)).getData()).isEmpty(); } @Test From 0dde9660829bbce65a30123cd954179be54c8650 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 15 May 2025 16:57:27 +0300 Subject: [PATCH 34/44] Support reprocessing for job with general error; job deletion; refactoring --- .../server/controller/JobController.java | 16 +++- .../entitiy/EntityStateSourcingListener.java | 21 ++++- .../server/service/job/DefaultJobManager.java | 34 ++++---- .../server/service/job/DummyJobProcessor.java | 1 + .../service/job/task/DummyTaskProcessor.java | 2 +- .../queue/DefaultTbClusterService.java | 3 +- .../server/service/job/JobManagerTest.java | 80 +++++++++++++++++-- ...anagerTest_EntityPartitioningStrategy.java | 2 +- .../server/cluster/TbClusterService.java | 2 + .../server/dao/job/JobService.java | 2 + .../data/job/DummyJobConfiguration.java | 2 + .../server/common/data/job/Job.java | 11 ++- .../common/data/job/JobConfiguration.java | 5 +- .../common/data/job/task/DummyTask.java | 2 +- .../common/data/job/task/DummyTaskResult.java | 34 ++++---- .../server/common/data/job/task/Task.java | 1 + .../common/data/job/task/TaskResult.java | 1 + .../msg/plugin/ComponentLifecycleMsg.java | 7 +- .../server/common/util/ProtoUtils.java | 6 ++ common/proto/src/main/proto/queue.proto | 1 + .../server/queue/task/TaskProcessor.java | 24 ++++-- .../thingsboard/common/util/JacksonUtil.java | 4 +- .../server/dao/job/DefaultJobService.java | 17 ++++ 23 files changed, 217 insertions(+), 61 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index 1db84593f5..b301c2e321 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -56,14 +57,14 @@ public class JobController extends BaseController { private final JobManager jobManager; @GetMapping("/job/{id}") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public Job getJobById(@PathVariable UUID id) throws ThingsboardException { // todo check permissions return jobService.findJobById(getTenantId(), new JobId(id)); } @GetMapping("/jobs") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public PageData getJobs(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @@ -86,17 +87,24 @@ public class JobController extends BaseController { } @PostMapping("/job/{id}/cancel") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public void cancelJob(@PathVariable UUID id) throws ThingsboardException { // todo check permissions jobManager.cancelJob(getTenantId(), new JobId(id)); } @PostMapping("/job/{id}/reprocess") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public void reprocessJob(@PathVariable UUID id) throws ThingsboardException { // todo check permissions jobManager.reprocessJob(getTenantId(), new JobId(id)); } + @DeleteMapping("/job/{id}") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + public void deleteJob(@PathVariable UUID id) throws ThingsboardException { + // todo check permissions + jobService.deleteJob(getTenantId(), new JobId(id)); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index bcb77e7005..83d7d60144 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; -import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -305,10 +304,24 @@ public class EntityStateSourcingListener { private void onJobUpdate(Job job) { jobManager.ifPresent(jobManager -> jobManager.onJobUpdate(job)); - if (job.getResult().getCancellationTs() > 0 || (job.getStatus().isOneOf(JobStatus.FAILED) && job.getResult().getGeneralError() != null)) { - // task processors will add this job to the list of discarded - tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); + + ComponentLifecycleEvent event; + if (job.getResult().getCancellationTs() > 0) { + event = ComponentLifecycleEvent.STOPPED; + } else if (job.getResult().getGeneralError() != null) { + event = ComponentLifecycleEvent.FAILED; + } else { + return; } + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(job.getTenantId()) + .entityId(job.getId()) + .event(event) + .info(JacksonUtil.newObjectNode() + .put("tasksKey", job.getConfiguration().getTasksKey())) + .build(); + // task processors will add this job to the list of discarded + tbClusterService.broadcast(msg); } private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 580dd0ff99..ac237d7310 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -178,26 +179,29 @@ public class DefaultJobManager implements JobManager { JobResult result = job.getResult(); if (result.getGeneralError() != null) { - throw new IllegalArgumentException("Reprocessing not allowed since job has general error"); - } - List taskFailures = result.getResults().stream() - .filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded()) - .toList(); - if (result.getFailedCount() > taskFailures.size()) { - throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")"); + job.presetResult(); + } else { + List taskFailures = result.getResults().stream() + .filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded()) + .toList(); + if (result.getFailedCount() > taskFailures.size()) { + throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")"); + } + result.setFailedCount(0); + result.setResults(result.getResults().stream() + .filter(TaskResult::isSuccess) + .toList()); + job.getConfiguration().setToReprocess(taskFailures); } - - result.setFailedCount(0); - result.setResults(result.getResults().stream() - .filter(TaskResult::isSuccess) - .toList()); - - job.getConfiguration().setToReprocess(taskFailures); - + job.getConfiguration().setTasksKey(UUID.randomUUID().toString()); jobService.saveJob(tenantId, job); } private void submitTask(Task task) { + if (ObjectUtils.anyNull(task.getTenantId(), task.getJobId(), task.getKey())) { + throw new IllegalArgumentException("Task " + task + " missing required fields"); + } + log.debug("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task); TaskProto taskProto = TaskProto.newBuilder() .setValue(JacksonUtil.toString(task)) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java index e1b91e606a..373bc4afee 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java @@ -76,6 +76,7 @@ public class DummyJobProcessor implements JobProcessor { return DummyTask.builder() .tenantId(job.getTenantId()) .jobId(job.getId()) + .key(configuration.getTasksKey()) .retries(configuration.getRetries()) .number(number) .processingTimeMs(configuration.getTaskProcessingTimeMs()) diff --git a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java index 1bcf6b36b3..5c88083146 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java @@ -36,7 +36,7 @@ public class DummyTaskProcessor extends TaskProcessor> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index a6e40333eb..3ae3bf5686 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFail import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.dao.job.JobService; +import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.task.JobStatsService; @@ -69,7 +69,7 @@ public class JobManagerTest extends AbstractControllerTest { private JobStatsService jobStatsService; @Autowired - private JobService jobService; + private JobDao jobDao; @Before public void setUp() throws Exception { @@ -220,7 +220,7 @@ public class JobManagerTest extends AbstractControllerTest { inv.callRealMethod(); } return null; - }).when(taskProcessor).addToDiscardedJobs(any()); // ignoring cancellation event, + }).when(taskProcessor).addToDiscarded(any()); // ignoring cancellation event, cancelJob(jobId); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -256,7 +256,7 @@ public class JobManagerTest extends AbstractControllerTest { Thread.sleep(3000); verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); - assertThat(jobService.findJobsByFilter(tenantId, JobFilter.builder().build(), new PageLink(100)).getData()).isEmpty(); + assertThat(jobDao.findByTenantIdAndFilter(tenantId, JobFilter.builder().build(), new PageLink(100)).getData()).isEmpty(); } @Test @@ -340,7 +340,7 @@ public class JobManagerTest extends AbstractControllerTest { } @Test - public void testGeneralJobError() { + public void testSubmitJob_generalError() { int submittedTasks = 100; JobId jobId = jobManager.submitJob(Job.builder() .tenantId(tenantId) @@ -358,7 +358,7 @@ public class JobManagerTest extends AbstractControllerTest { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); assertThat(job.getResult().getSuccessfulCount()).isBetween(1, submittedTasks); - assertThat(job.getResult().getDiscardedCount()).isBetween(1, submittedTasks); + assertThat(job.getResult().getDiscardedCount()).isZero(); assertThat(job.getResult().getTotalCount()).isNull(); }); @@ -369,7 +369,70 @@ public class JobManagerTest extends AbstractControllerTest { } @Test - public void testJobReprocessing() throws Exception { + public void testSubmitJob_immediateGeneralError() { + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("Test job") + .configuration(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(0) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + assertThat(job.getResult().getSuccessfulCount()).isZero(); + assertThat(job.getResult().getDiscardedCount()).isZero(); + assertThat(job.getResult().getFailedCount()).isZero(); + assertThat(job.getResult().getTotalCount()).isNull(); + }); + } + + @Test + public void testReprocessJob_generalError() throws Exception { + int submittedTasks = 100; + JobId jobId = jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key("test-job") + .description("Test job") + .configuration(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(submittedTasks) + .taskProcessingTimeMs(10) + .build()) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); + assertThat(job.getResult().getGeneralError()).isEqualTo("Some error while submitting tasks"); + }); + + Job savedJob = jobDao.findById(tenantId, jobId.getId()); + DummyJobConfiguration configuration = savedJob.getConfiguration(); + configuration.setGeneralError(null); + configuration.setSuccessfulTasksCount(submittedTasks); + jobDao.save(tenantId, savedJob); + + reprocessJob(jobId); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(job.getResult().getGeneralError()).isNull(); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(submittedTasks); + assertThat(job.getResult().getTotalCount()).isEqualTo(submittedTasks); + assertThat(job.getResult().getFailedCount()).isZero(); + assertThat(job.getResult().getDiscardedCount()).isZero(); + }); + } + + @Test + public void testReprocessJob() throws Exception { int successfulTasks = 3; int failedTasks = 2; int totalTasksCount = successfulTasks + failedTasks; @@ -410,11 +473,12 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getFailedCount()).isZero(); assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount); assertThat(job.getResult().getResults()).isEmpty(); + assertThat(job.getConfiguration().getToReprocess()).isNullOrEmpty(); }); } @Test - public void testJobReprocessing_somePermanentlyFailed() throws Exception { + public void testReprocessJob_somePermanentlyFailed() throws Exception { int successfulTasks = 3; int failedTasks = 2; int permanentlyFailedTasks = 1; diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java index a021603ca6..59093ad802 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java @@ -36,7 +36,7 @@ public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest { } @Override - public void testGeneralJobError() { + public void testSubmitJob_generalError() { } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index aed6eb4cf5..6da6fcf8a8 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -87,6 +87,8 @@ public interface TbClusterService extends TbQueueClusterService { void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); + void broadcast(ComponentLifecycleMsg componentLifecycleMsg); + void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback); void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 33f9511267..7d4c68f6a4 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -40,4 +40,6 @@ public interface JobService extends EntityDaoService { Job findLatestJobByKey(TenantId tenantId, String key); + void deleteJob(TenantId tenantId, JobId jobId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java index 62695eb237..a9ff9e7f4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -20,6 +20,7 @@ import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.List; @@ -28,6 +29,7 @@ import java.util.List; @AllArgsConstructor @NoArgsConstructor @Builder +@ToString(callSuper = true) public class DummyJobConfiguration extends JobConfiguration { private long taskProcessingTimeMs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index d4e69761c8..f914067be3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.UUID; + @Data @NoArgsConstructor @ToString(callSuper = true) @@ -42,19 +44,26 @@ public class Job extends BaseData implements HasTenantId { private String key; @NotBlank private String description; + @NotNull private JobStatus status; @NotNull @Valid private JobConfiguration configuration; + @NotNull private JobResult result; - @Builder + @Builder(toBuilder = true) public Job(TenantId tenantId, JobType type, String key, String description, JobConfiguration configuration) { this.tenantId = tenantId; this.type = type; this.key = key; this.description = description; this.configuration = configuration; + this.configuration.setTasksKey(UUID.randomUUID().toString()); + presetResult(); + } + + public void presetResult() { this.result = switch (type) { case DUMMY -> new DummyJobResult(); }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 35f44cd4be..54d8166a5d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.server.common.data.job.task.TaskResult; @@ -34,7 +35,9 @@ import java.util.List; @Data public abstract class JobConfiguration implements Serializable { - private List toReprocess; + @NotBlank + private String tasksKey; // internal + private List toReprocess; // internal @JsonIgnore public abstract JobType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java index 7e262ed7b8..e0f670ad9e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTask.java @@ -46,7 +46,7 @@ public class DummyTask extends Task { @Override public DummyTaskResult toDiscarded() { - return DummyTaskResult.discarded(); + return DummyTaskResult.discarded(this); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java index cd68b4d248..1988f13eb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.job.task; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.job.JobType; @@ -25,29 +26,34 @@ import org.thingsboard.server.common.data.job.JobType; @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @SuperBuilder +@ToString(callSuper = true) public class DummyTaskResult extends TaskResult { - private static final DummyTaskResult SUCCESS = DummyTaskResult.builder().success(true).build(); - private static final DummyTaskResult DISCARDED = DummyTaskResult.builder().discarded(true).build(); - private DummyTaskFailure failure; - public static DummyTaskResult success() { - return SUCCESS; + public static DummyTaskResult success(DummyTask task) { + return DummyTaskResult.builder() + .key(task.getKey()) + .success(true) + .build(); } public static DummyTaskResult failed(DummyTask task, Throwable error) { - DummyTaskResult result = new DummyTaskResult(); - result.setFailure(DummyTaskFailure.builder() - .error(error.getMessage()) - .number(task.getNumber()) - .failAlways(task.isFailAlways()) - .build()); - return result; + return DummyTaskResult.builder() + .key(task.getKey()) + .failure(DummyTaskFailure.builder() + .error(error.getMessage()) + .number(task.getNumber()) + .failAlways(task.isFailAlways()) + .build()) + .build(); } - public static DummyTaskResult discarded() { - return DISCARDED; + public static DummyTaskResult discarded(DummyTask task) { + return DummyTaskResult.builder() + .key(task.getKey()) + .discarded(true) + .build(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java index cce32fdad0..fb373d9850 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/Task.java @@ -40,6 +40,7 @@ public abstract class Task { private TenantId tenantId; private JobId jobId; + private String key; private int retries; public Task() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 9416cb8f6b..21303a55fe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.job.JobType; }) public abstract class TaskResult { + private String key; private boolean success; private boolean discarded; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index 13fd6159fc..d57301fd10 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.msg.plugin; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -45,13 +46,14 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { private final String name; private final EntityId oldProfileId; private final EntityId profileId; + private final JsonNode info; public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) { - this(tenantId, entityId, event, null, null, null, null); + this(tenantId, entityId, event, null, null, null, null, null); } @Builder - private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId) { + private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, JsonNode info) { this.tenantId = tenantId; this.entityId = entityId; this.event = event; @@ -59,6 +61,7 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { this.name = name; this.oldProfileId = oldProfileId; this.profileId = profileId; + this.info = info; } public Optional getRuleChainId() { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 1ebd753f3c..9781cf9a7e 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -139,6 +139,9 @@ public class ProtoUtils { if (msg.getOldName() != null) { builder.setOldName(msg.getOldName()); } + if (msg.getInfo() != null) { + builder.setInfo(JacksonUtil.toString(msg.getInfo())); + } return builder.build(); } @@ -166,6 +169,9 @@ public class ProtoUtils { var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB()))); } + if (proto.hasInfo()) { + builder.info(JacksonUtil.toJsonNode(proto.getInfo())); + } return builder.build(); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2a7d28241e..d03f59143c 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1261,6 +1261,7 @@ message ComponentLifecycleMsgProto { int64 oldProfileIdLSB = 10; int64 profileIdMSB = 11; int64 profileIdLSB = 12; + optional string info = 13; } message EdgeEventMsgProto { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 947d252442..ef0e48e30a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -68,7 +68,9 @@ public abstract class TaskProcessor, R extends TaskResult> { private MainQueueConsumerManager, QueueConfig> taskConsumer; private final ExecutorService taskExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); - private final SetCache discardedJobs = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); + private final SetCache discarded = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); + private final SetCache failed = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); + private final SetCache deletedTenants = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); @PostConstruct @@ -98,9 +100,13 @@ public abstract class TaskProcessor, R extends TaskResult> { EntityId entityId = event.getEntityId(); switch (entityId.getEntityType()) { case JOB -> { + String tasksKey = event.getInfo().get("tasksKey").asText(); if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { - log.info("Adding job {} to discarded", entityId); - addToDiscardedJobs(entityId.getId()); + log.info("Adding job {} ({}) to discarded", entityId, tasksKey); + addToDiscarded(tasksKey); + } else if (event.getEvent() == ComponentLifecycleEvent.FAILED) { + log.info("Adding job {} ({}) to failed", entityId, tasksKey); + failed.add(tasksKey); } } case TENANT -> { @@ -117,14 +123,18 @@ public abstract class TaskProcessor, R extends TaskResult> { try { @SuppressWarnings("unchecked") T task = (T) JacksonUtil.fromString(msg.getValue().getValue(), Task.class); - if (discardedJobs.contains(task.getJobId().getId())) { - log.debug("Skipping task for cancelled job {}: {}", task.getJobId(), task); + if (discarded.contains(task.getKey())) { + log.debug("Skipping task for discarded job {}: {}", task.getJobId(), task); reportTaskDiscarded(task); continue; + } else if (failed.contains(task.getKey())) { + log.debug("Skipping task for failed job {}: {}", task.getJobId(), task); + continue; } else if (deletedTenants.contains(task.getTenantId().getId())) { log.debug("Skipping task for deleted tenant {}: {}", task.getTenantId(), task); continue; } + processTask(task); } catch (InterruptedException e) { throw e; @@ -185,8 +195,8 @@ public abstract class TaskProcessor, R extends TaskResult> { statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } - public void addToDiscardedJobs(UUID jobId) { - discardedJobs.add(jobId); + public void addToDiscarded(String tasksKey) { + discarded.add(tasksKey); } protected V wait(Future future) throws Exception { diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 5903c10ac2..d153501b92 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -37,9 +37,10 @@ import com.google.common.collect.Lists; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Contract; +import org.thingsboard.server.common.data.Views; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.Views; import java.io.File; import java.io.IOException; @@ -109,6 +110,7 @@ public class JacksonUtil { } } + @Contract("null, _ -> null") // so that IDE doesn't show NPE warning when input is not null public static T fromString(String string, Class clazz) { try { return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 50b3e4bde8..cd001b61b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.Optional; @@ -119,6 +120,11 @@ public class DefaultJobService extends AbstractEntityService implements JobServi boolean publishEvent = false; for (TaskResult taskResult : jobStats.getTaskResults()) { + if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) { + log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey()); + continue; + } + result.processTaskResult(taskResult); if (result.getCancellationTs() > 0) { @@ -142,6 +148,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi publishEvent = true; } result.setFinishTs(System.currentTimeMillis()); + job.getConfiguration().setToReprocess(null); } } @@ -149,6 +156,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } private Job saveJob(TenantId tenantId, Job job, boolean publishEvent, JobStatus prevStatus) { + ConstraintValidator.validateFields(job); job = jobDao.save(tenantId, job); if (publishEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder() @@ -186,6 +194,15 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return jobDao.findLatestByTenantIdAndKey(tenantId, key); } + @Override + public void deleteJob(TenantId tenantId, JobId jobId) { + Job job = findJobById(tenantId, jobId); + if (!job.getStatus().isOneOf(CANCELLED, COMPLETED, FAILED)) { + throw new IllegalArgumentException("Job must be cancelled, completed or failed"); + } + jobDao.removeById(tenantId, jobId.getId()); + } + private Job findForUpdate(TenantId tenantId, JobId jobId) { return jobDao.findByIdForUpdate(tenantId, jobId); } From d54e7a0e78f0da900aa7dc3274ac1097fdb7919d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 15 May 2025 17:15:06 +0300 Subject: [PATCH 35/44] Refactor BaseController.checkEntityId --- .../server/controller/BaseController.java | 109 +++++------------- 1 file changed, 30 insertions(+), 79 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 73e278389a..620c773913 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -385,7 +385,7 @@ public abstract class BaseController { public void handleControllerException(Exception e, HttpServletResponse response) { ThingsboardException thingsboardException = handleException(e); if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception - && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { + && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { e = (Exception) thingsboardException.getCause(); } else { e = thingsboardException; @@ -430,7 +430,7 @@ public abstract class BaseController { if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException - || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { + || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); } else if (exception instanceof MessagingException) { return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); @@ -602,88 +602,39 @@ public abstract class BaseController { } } - protected void checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException { + protected HasId checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException { try { if (entityId == null) { throw new ThingsboardException("Parameter entityId can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } validateId(entityId.getId(), id -> "Incorrect entityId " + id); - switch (entityId.getEntityType()) { - case ALARM: - checkAlarmId(new AlarmId(entityId.getId()), operation); - return; - case DEVICE: - checkDeviceId(new DeviceId(entityId.getId()), operation); - return; - case DEVICE_PROFILE: - checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation); - return; - case CUSTOMER: - checkCustomerId(new CustomerId(entityId.getId()), operation); - return; - case TENANT: - checkTenantId(TenantId.fromUUID(entityId.getId()), operation); - return; - case TENANT_PROFILE: - checkTenantProfileId(new TenantProfileId(entityId.getId()), operation); - return; - case RULE_CHAIN: - checkRuleChain(new RuleChainId(entityId.getId()), operation); - return; - case RULE_NODE: - checkRuleNode(new RuleNodeId(entityId.getId()), operation); - return; - case ASSET: - checkAssetId(new AssetId(entityId.getId()), operation); - return; - case ASSET_PROFILE: - checkAssetProfileId(new AssetProfileId(entityId.getId()), operation); - return; - case DASHBOARD: - checkDashboardId(new DashboardId(entityId.getId()), operation); - return; - case USER: - checkUserId(new UserId(entityId.getId()), operation); - return; - case ENTITY_VIEW: - checkEntityViewId(new EntityViewId(entityId.getId()), operation); - return; - case EDGE: - checkEdgeId(new EdgeId(entityId.getId()), operation); - return; - case WIDGETS_BUNDLE: - checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation); - return; - case WIDGET_TYPE: - checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation); - return; - case TB_RESOURCE: - checkResourceInfoId(new TbResourceId(entityId.getId()), operation); - return; - case OTA_PACKAGE: - checkOtaPackageId(new OtaPackageId(entityId.getId()), operation); - return; - case QUEUE: - checkQueueId(new QueueId(entityId.getId()), operation); - return; - case OAUTH2_CLIENT: - checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation); - return; - case DOMAIN: - checkDomainId(new DomainId(entityId.getId()), operation); - return; - case MOBILE_APP: - checkMobileAppId(new MobileAppId(entityId.getId()), operation); - return; - case MOBILE_APP_BUNDLE: - checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); - return; - case CALCULATED_FIELD: - checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); - return; - default: - checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); - } + return switch (entityId.getEntityType()) { + case ALARM -> checkAlarmId(new AlarmId(entityId.getId()), operation); + case DEVICE -> checkDeviceId(new DeviceId(entityId.getId()), operation); + case DEVICE_PROFILE -> checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation); + case CUSTOMER -> checkCustomerId(new CustomerId(entityId.getId()), operation); + case TENANT -> checkTenantId(TenantId.fromUUID(entityId.getId()), operation); + case TENANT_PROFILE -> checkTenantProfileId(new TenantProfileId(entityId.getId()), operation); + case RULE_CHAIN -> checkRuleChain(new RuleChainId(entityId.getId()), operation); + case RULE_NODE -> checkRuleNode(new RuleNodeId(entityId.getId()), operation); + case ASSET -> checkAssetId(new AssetId(entityId.getId()), operation); + case ASSET_PROFILE -> checkAssetProfileId(new AssetProfileId(entityId.getId()), operation); + case DASHBOARD -> checkDashboardId(new DashboardId(entityId.getId()), operation); + case USER -> checkUserId(new UserId(entityId.getId()), operation); + case ENTITY_VIEW -> checkEntityViewId(new EntityViewId(entityId.getId()), operation); + case EDGE -> checkEdgeId(new EdgeId(entityId.getId()), operation); + case WIDGETS_BUNDLE -> checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation); + case WIDGET_TYPE -> checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation); + case TB_RESOURCE -> checkResourceInfoId(new TbResourceId(entityId.getId()), operation); + case OTA_PACKAGE -> checkOtaPackageId(new OtaPackageId(entityId.getId()), operation); + case QUEUE -> checkQueueId(new QueueId(entityId.getId()), operation); + case OAUTH2_CLIENT -> checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation); + case DOMAIN -> checkDomainId(new DomainId(entityId.getId()), operation); + case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); + case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); + case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); + default -> (HasId) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); + }; } catch (Exception e) { throw handleException(e, false); } From 325c71f2ab7e5fab7419bd31dfed99e0ff570b4b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 15 May 2025 18:25:17 +0300 Subject: [PATCH 36/44] Fix checkCalculatedFieldId --- .../java/org/thingsboard/server/controller/BaseController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 51735b6a9a..e5ea69dcb5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -908,12 +908,13 @@ public abstract class BaseController { } } - private void checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException { + private CalculatedField checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException { validateId(calculatedFieldId, "Invalid entity id"); SecurityUser user = getCurrentUser(); CalculatedField cf = calculatedFieldService.findById(user.getTenantId(), calculatedFieldId); checkNotNull(cf, calculatedFieldId.getEntityType().getNormalName() + " with id [" + calculatedFieldId + "] is not found"); checkEntityId(cf.getEntityId(), operation); + return cf; } protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) { From 5e46608abc718baccc1ecae05dce187655f81f93 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 19 May 2025 15:30:34 +0300 Subject: [PATCH 37/44] Support for job manager on Rule Engine --- .../server/actors/ActorSystemContext.java | 8 +- .../actors/ruleChain/DefaultTbContext.java | 8 +- .../server/controller/JobController.java | 2 +- .../entitiy/EntityStateSourcingListener.java | 7 +- .../server/service/job/DefaultJobManager.java | 65 +--------- .../server/service/job/JobStatsProcessor.java | 115 ++++++++++++++++++ .../server/service/job/JobManagerTest.java | 1 + .../InMemoryMonolithQueueFactory.java | 6 - .../provider/KafkaMonolithQueueFactory.java | 12 -- .../provider/KafkaTbCoreQueueFactory.java | 12 -- .../queue/provider/TbCoreQueueFactory.java | 4 - .../InMemoryTaskProducerQueueFactory.java | 40 ++++++ .../task/KafkaTaskProducerQueueFactory.java | 62 ++++++++++ .../queue/task/TaskProducerQueueFactory.java | 27 ++++ .../rule/engine/api}/JobManager.java | 2 +- .../rule/engine/api/TbContext.java | 4 +- 16 files changed, 269 insertions(+), 106 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProducerQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProducerQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProducerQueueFactory.java rename {application/src/main/java/org/thingsboard/server/service/job => rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api}/JobManager.java (95%) 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 228928123d..396ecd3771 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.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.DeviceStateManager; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; @@ -553,11 +554,14 @@ public class ActorSystemContext { @Getter private CalculatedFieldQueueService calculatedFieldQueueService; - @Lazy - @Autowired(required = false) + @Autowired @Getter private JobService jobService; + @Autowired + @Getter + private JobManager jobManager; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private int maxConcurrentSessionsPerDevice; 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 d443b7e131..dff0cd4cf1 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 @@ -24,6 +24,7 @@ import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.DeviceStateManager; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; @@ -93,6 +94,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; @@ -108,7 +110,6 @@ import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; @@ -895,6 +896,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getJobService(); } + @Override + public JobManager getJobManager() { + return mainCtx.getJobManager(); + } + @Override public boolean isExternalNodeForceAck() { return mainCtx.isExternalNodeForceAck(); diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index b301c2e321..9719306823 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.job.JobManager; +import org.thingsboard.rule.engine.api.JobManager; import java.util.List; import java.util.UUID; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 83d7d60144..03ab77ac09 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -59,9 +59,8 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.service.job.JobManager; +import org.thingsboard.rule.engine.api.JobManager; -import java.util.Optional; import java.util.Set; @Slf4j @@ -72,7 +71,7 @@ public class EntityStateSourcingListener { private final TenantService tenantService; private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; - private final Optional jobManager; + private final JobManager jobManager; @PostConstruct public void init() { @@ -303,7 +302,7 @@ public class EntityStateSourcingListener { } private void onJobUpdate(Job job) { - jobManager.ifPresent(jobManager -> jobManager.onJobUpdate(job)); + jobManager.onJobUpdate(job); ComponentLifecycleEvent event; if (job.getResult().getCancellationTs() > 0) { diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index ac237d7310..035de3c1d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -16,20 +16,18 @@ package org.thingsboard.server.service.job; import jakarta.annotation.PreDestroy; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobResult; -import org.thingsboard.server.common.data.job.JobStats; import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; @@ -41,28 +39,22 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.notification.DefaultNotifications; -import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.task.JobStatsService; -import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.task.TaskProducerQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.function.Function; import java.util.stream.Collectors; @@ -78,12 +70,10 @@ public class DefaultJobManager implements JobManager { private final TasksQueueConfig queueConfig; private final Map jobProcessors; private final Map>> taskProducers; - private final QueueConsumerManager> jobStatsConsumer; private final ExecutorService executor; - private final ExecutorService consumerExecutor; public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, NotificationCenter notificationCenter, - PartitionService partitionService, TbCoreQueueFactory queueFactory, TasksQueueConfig queueConfig, + PartitionService partitionService, TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, List jobProcessors) { this.jobService = jobService; this.jobStatsService = jobStatsService; @@ -93,20 +83,6 @@ public class DefaultJobManager implements JobManager { this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); - this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("job-stats-consumer")); - this.jobStatsConsumer = QueueConsumerManager.>builder() - .name("job-stats") - .msgPackProcessor(this::processStats) - .pollInterval(queueConfig.getStatsPollInterval()) - .consumerCreator(queueFactory::createJobStatsConsumer) - .consumerExecutor(consumerExecutor) - .build(); - } - - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void afterStartUp() { - jobStatsConsumer.subscribe(); - jobStatsConsumer.launch(); } @Override @@ -229,39 +205,6 @@ public class DefaultJobManager implements JobManager { }); } - @SneakyThrows - private void processStats(List> msgs, TbQueueConsumer> consumer) { - Map stats = new HashMap<>(); - - for (TbProtoQueueMsg msg : msgs) { - JobStatsMsg statsMsg = msg.getValue(); - TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); - JobId jobId = new JobId(new UUID(statsMsg.getJobIdMSB(), statsMsg.getJobIdLSB())); - JobStats jobStats = stats.computeIfAbsent(jobId, __ -> new JobStats(tenantId, jobId)); - - if (statsMsg.hasTaskResult()) { - TaskResult taskResult = JacksonUtil.fromString(statsMsg.getTaskResult().getValue(), TaskResult.class); - jobStats.getTaskResults().add(taskResult); - } - if (statsMsg.hasTotalTasksCount()) { - jobStats.setTotalTasksCount(statsMsg.getTotalTasksCount()); - } - } - - stats.forEach((jobId, jobStats) -> { - TenantId tenantId = jobStats.getTenantId(); - try { - log.debug("[{}][{}] Processing job stats: {}", tenantId, jobId, stats); - jobService.processStats(tenantId, jobId, jobStats); - } catch (Exception e) { - log.error("[{}][{}] Failed to process job stats: {}", tenantId, jobId, jobStats, e); - } - }); - consumer.commit(); - - Thread.sleep(queueConfig.getStatsProcessingInterval()); - } - private void sendJobFinishedNotification(Job job) { NotificationTemplate template = DefaultNotifications.DefaultNotification.builder() .name("Job finished") @@ -284,9 +227,7 @@ public class DefaultJobManager implements JobManager { @PreDestroy private void destroy() { - jobStatsConsumer.stop(); executor.shutdownNow(); - consumerExecutor.shutdownNow(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java new file mode 100644 index 0000000000..c5b2577819 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java @@ -0,0 +1,115 @@ +/** + * 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.job; + +import jakarta.annotation.PreDestroy; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.JobId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.JobStats; +import org.thingsboard.server.common.data.job.task.TaskResult; +import org.thingsboard.server.dao.job.JobService; +import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.settings.TasksQueueConfig; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@TbCoreComponent +@Component +@Slf4j +public class JobStatsProcessor { + + private final JobService jobService; + private final TasksQueueConfig queueConfig; + private final QueueConsumerManager> jobStatsConsumer; + private final ExecutorService consumerExecutor; + + public JobStatsProcessor(JobService jobService, + TasksQueueConfig queueConfig, + TbCoreQueueFactory queueFactory) { + this.jobService = jobService; + this.queueConfig = queueConfig; + this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("job-stats-consumer")); + this.jobStatsConsumer = QueueConsumerManager.>builder() + .name("job-stats") + .msgPackProcessor(this::processStats) + .pollInterval(queueConfig.getStatsPollInterval()) + .consumerCreator(queueFactory::createJobStatsConsumer) + .consumerExecutor(consumerExecutor) + .build(); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + jobStatsConsumer.subscribe(); + jobStatsConsumer.launch(); + } + + @SneakyThrows + private void processStats(List> msgs, TbQueueConsumer> consumer) { + Map stats = new HashMap<>(); + + for (TbProtoQueueMsg msg : msgs) { + JobStatsMsg statsMsg = msg.getValue(); + TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); + JobId jobId = new JobId(new UUID(statsMsg.getJobIdMSB(), statsMsg.getJobIdLSB())); + JobStats jobStats = stats.computeIfAbsent(jobId, __ -> new JobStats(tenantId, jobId)); + + if (statsMsg.hasTaskResult()) { + TaskResult taskResult = JacksonUtil.fromString(statsMsg.getTaskResult().getValue(), TaskResult.class); + jobStats.getTaskResults().add(taskResult); + } + if (statsMsg.hasTotalTasksCount()) { + jobStats.setTotalTasksCount(statsMsg.getTotalTasksCount()); + } + } + + stats.forEach((jobId, jobStats) -> { + TenantId tenantId = jobStats.getTenantId(); + try { + log.debug("[{}][{}] Processing job stats: {}", tenantId, jobId, stats); + jobService.processStats(tenantId, jobId, jobStats); + } catch (Exception e) { + log.error("[{}][{}] Failed to process job stats: {}", tenantId, jobId, jobStats, e); + } + }); + consumer.commit(); + + Thread.sleep(queueConfig.getStatsProcessingInterval()); + } + + @PreDestroy + private void destroy() { + jobStatsConsumer.stop(); + consumerExecutor.shutdownNow(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 3ae3bf5686..dd19ddcea1 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -23,6 +23,7 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index ddaf34715f..d3233f8dee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -262,11 +261,6 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE .build(); } - @Override - public TbQueueProducer> createTaskProducer(JobType jobType) { - return new InMemoryTbQueueProducer<>(storage, jobType.getTasksTopic()); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return new InMemoryTbQueueConsumer<>(storage, tasksQueueConfig.getStatsTopic()); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 902848f65f..866f8d235e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -23,7 +23,6 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -31,7 +30,6 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -650,16 +648,6 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .build(); } - @Override - public TbQueueProducer> createTaskProducer(JobType jobType) { - return TbKafkaProducerTemplate.>builder() - .clientId(jobType.name().toLowerCase() + "-task-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName(jobType.getTasksTopic())) - .settings(kafkaSettings) - .admin(tasksAdmin) - .build(); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 048b08f15a..70009aa29d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -22,12 +22,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -529,16 +527,6 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .build(); } - @Override - public TbQueueProducer> createTaskProducer(JobType jobType) { - return TbKafkaProducerTemplate.>builder() - .clientId(jobType.name().toLowerCase() + "-task-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName(jobType.getTasksTopic())) - .settings(kafkaSettings) - .admin(tasksAdmin) - .build(); - } - @Override public TbQueueConsumer> createJobStatsConsumer() { return TbKafkaConsumerTemplate.>builder() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index e47354941c..37c15d5b87 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -17,10 +17,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -168,8 +166,6 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous TbQueueProducer> createToCalculatedFieldNotificationMsgProducer(); - TbQueueProducer> createTaskProducer(JobType jobType); - TbQueueConsumer> createJobStatsConsumer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProducerQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProducerQueueFactory.java new file mode 100644 index 0000000000..7f0cae7eb1 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/InMemoryTaskProducerQueueFactory.java @@ -0,0 +1,40 @@ +/** + * 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.queue.task; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; + +@Component +@ConditionalOnExpression("'${queue.type:null}' == 'in-memory'") +@RequiredArgsConstructor +public class InMemoryTaskProducerQueueFactory implements TaskProducerQueueFactory { + + private final InMemoryStorage storage; + + @Override + public TbQueueProducer> createTaskProducer(JobType jobType) { + return new InMemoryTbQueueProducer<>(storage, jobType.getTasksTopic()); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProducerQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProducerQueueFactory.java new file mode 100644 index 0000000000..b19db211fe --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/KafkaTaskProducerQueueFactory.java @@ -0,0 +1,62 @@ +/** + * 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.queue.task; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; +import org.thingsboard.server.queue.kafka.TbKafkaSettings; +import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; + +@Component +@ConditionalOnExpression("'${queue.type:null}' == 'kafka' && ('${service.type:null}' == 'monolith' || " + + "'${service.type:null}' == 'tb-core' || '${service.type:null}' == 'tb-rule-engine')") +public class KafkaTaskProducerQueueFactory implements TaskProducerQueueFactory { + + private final TopicService topicService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TbKafkaSettings kafkaSettings; + private final TbQueueAdmin tasksAdmin; + + KafkaTaskProducerQueueFactory(TopicService topicService, + TbServiceInfoProvider serviceInfoProvider, + TbKafkaSettings kafkaSettings, + TbKafkaTopicConfigs kafkaTopicConfigs) { + this.topicService = topicService; + this.kafkaSettings = kafkaSettings; + this.serviceInfoProvider = serviceInfoProvider; + this.tasksAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTasksConfigs()); + } + + @Override + public TbQueueProducer> createTaskProducer(JobType jobType) { + return TbKafkaProducerTemplate.>builder() + .clientId(jobType.name().toLowerCase() + "-task-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(jobType.getTasksTopic())) + .settings(kafkaSettings) + .admin(tasksAdmin) + .build(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProducerQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProducerQueueFactory.java new file mode 100644 index 0000000000..ffb64a07ce --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProducerQueueFactory.java @@ -0,0 +1,27 @@ +/** + * 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.queue.task; + +import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; + +public interface TaskProducerQueueFactory { + + TbQueueProducer> createTaskProducer(JobType jobType); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/job/JobManager.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index 8e4858ebe3..3ee29dd7c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.job; +package org.thingsboard.rule.engine.api; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; 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 4b38975504..16f2936964 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 @@ -62,6 +62,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; @@ -77,7 +78,6 @@ import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; @@ -365,6 +365,8 @@ public interface TbContext { JobService getJobService(); + JobManager getJobManager(); + boolean isExternalNodeForceAck(); /** From df2d8cc895fb3b826afd37aadb760a18996cd4d4 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 19 May 2025 15:35:18 +0300 Subject: [PATCH 38/44] Remove TbCoreComponent from DefaultJobManager --- .../org/thingsboard/server/service/job/DefaultJobManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 035de3c1d7..314581acef 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -48,7 +48,6 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.settings.TasksQueueConfig; import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.queue.task.TaskProducerQueueFactory; -import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Arrays; import java.util.List; @@ -58,7 +57,6 @@ import java.util.concurrent.ExecutorService; import java.util.function.Function; import java.util.stream.Collectors; -@TbCoreComponent @Component @Slf4j public class DefaultJobManager implements JobManager { From b618249c6246ef7bb784f57715009da5ad290131 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 May 2025 13:50:18 +0300 Subject: [PATCH 39/44] Introduce entity id for jobs --- .../server/controller/JobController.java | 10 +- .../processor/JobsDeletionTaskProcessor.java | 43 ++++ .../server/service/job/DefaultJobManager.java | 32 +-- .../server/service/job/JobProcessor.java | 2 +- .../server/controller/AbstractWebTest.java | 5 +- .../server/service/job/JobManagerTest.java | 236 ++++++------------ .../server/dao/job/JobService.java | 3 + .../data/housekeeper/HousekeeperTask.java | 4 + .../data/housekeeper/HousekeeperTaskType.java | 3 +- .../common/data/job/DummyJobResult.java | 8 - .../server/common/data/job/Job.java | 15 +- .../server/common/data/job/JobFilter.java | 4 + .../server/common/data/job/JobResult.java | 3 - .../dao/housekeeper/CleanUpService.java | 4 + .../server/dao/job/DefaultJobService.java | 11 +- .../thingsboard/server/dao/job/JobDao.java | 7 +- .../server/dao/model/ModelConstants.java | 3 +- .../server/dao/model/sql/JobEntity.java | 15 +- .../server/dao/sql/job/JobRepository.java | 32 ++- .../server/dao/sql/job/JpaJobDao.java | 21 +- .../main/resources/sql/schema-entities.sql | 3 +- .../rule/engine/api/JobManager.java | 2 +- 22 files changed, 237 insertions(+), 229 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index 9719306823..55c68a461f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; @@ -36,7 +37,6 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.rule.engine.api.JobManager; import java.util.List; import java.util.UUID; @@ -76,12 +76,18 @@ public class JobController extends BaseController { @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder, @RequestParam(required = false) List types, - @RequestParam(required = false) List statuses) throws ThingsboardException { + @RequestParam(required = false) List statuses, + @RequestParam(required = false) List entities, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { // todo check permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); JobFilter filter = JobFilter.builder() .types(types) .statuses(statuses) + .entities(entities) + .startTime(startTime) + .endTime(endTime) .build(); return jobService.findJobsByFilter(getTenantId(), filter, pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java new file mode 100644 index 0000000000..f0f829770a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java @@ -0,0 +1,43 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.dao.job.JobService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JobsDeletionTaskProcessor extends HousekeeperTaskProcessor { + + private final JobService jobService; + + @Override + public void process(HousekeeperTask task) throws Exception { + int deletedCount = jobService.deleteJobsByEntityId(task.getTenantId(), task.getEntityId()); + log.debug("[{}][{}][{}] Deleted {} jobs", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_JOBS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 314581acef..379e72d28b 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -22,7 +22,6 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.JobManager; -import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -32,13 +31,9 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; -import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo; -import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; -import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; -import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -63,19 +58,17 @@ public class DefaultJobManager implements JobManager { private final JobService jobService; private final JobStatsService jobStatsService; - private final NotificationCenter notificationCenter; private final PartitionService partitionService; private final TasksQueueConfig queueConfig; private final Map jobProcessors; private final Map>> taskProducers; private final ExecutorService executor; - public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, NotificationCenter notificationCenter, - PartitionService partitionService, TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, + public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService, + TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, List jobProcessors) { this.jobService = jobService; this.jobStatsService = jobStatsService; - this.notificationCenter = notificationCenter; this.partitionService = partitionService; this.queueConfig = queueConfig; this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); @@ -105,10 +98,7 @@ public class DefaultJobManager implements JobManager { case COMPLETED, FAILED -> { executor.execute(() -> { try { - if (status == JobStatus.COMPLETED) { - getJobProcessor(job.getType()).onJobCompleted(job); - } - sendJobFinishedNotification(job); + getJobProcessor(job.getType()).onJobFinished(job); } catch (Throwable e) { log.error("Failed to process job update: {}", job, e); } @@ -203,22 +193,6 @@ public class DefaultJobManager implements JobManager { }); } - private void sendJobFinishedNotification(Job job) { - NotificationTemplate template = DefaultNotifications.DefaultNotification.builder() - .name("Job finished") - .subject("${type} task ${status}") - .text("${description} ${status}: ${result}") - .build().toTemplate(); - GeneralNotificationInfo info = new GeneralNotificationInfo(Map.of( - "type", job.getType().getTitle(), - "description", job.getDescription(), - "status", job.getStatus().name().toLowerCase(), - "result", job.getResult().getDescription() - )); - // todo: button to see details (forward to jobs page) - notificationCenter.sendGeneralWebNotification(job.getTenantId(), new TenantAdministratorsFilter(), template, info); - } - private JobProcessor getJobProcessor(JobType jobType) { return jobProcessors.get(jobType); } diff --git a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java index 16ef5e404f..e33878d008 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java @@ -29,7 +29,7 @@ public interface JobProcessor { void reprocess(Job job, List taskFailures, Consumer> taskConsumer) throws Exception; - default void onJobCompleted(Job job) {} + default void onJobFinished(Job job) {} JobType getType(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index ef658fc17a..b93ff0f623 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1272,8 +1272,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doGetTypedWithPageLink("/api/jobs?", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } - protected List findJobs(JobType... types) throws Exception { - return doGetTypedWithPageLink("/api/jobs?types=" + Arrays.stream(types).map(Enum::name).collect(Collectors.joining(",")) + "&", + protected List findJobs(List types, List entities) throws Exception { + return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index dd19ddcea1..14c0e4e847 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.job; -import org.assertj.core.api.ThrowingConsumer; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -24,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.DummyJobConfiguration; @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; -import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobDao; @@ -53,6 +52,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @TestPropertySource(properties = { @@ -72,9 +72,14 @@ public class JobManagerTest extends AbstractControllerTest { @Autowired private JobDao jobDao; + private TenantId tenantId; + private Device jobEntity; + @Before public void setUp() throws Exception { loginTenantAdmin(); + tenantId = super.tenantId; + jobEntity = createDevice("Test", "Test"); } @After @@ -84,15 +89,9 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testSubmitJob_allTasksSuccessful() { int tasksCount = 5; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(1000) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -108,29 +107,18 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); }); - - checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job task completed"); - assertThat(notification.getText()).isEqualTo("Test job completed: 5/5 successful, 0 failed"); - }); } @Test public void testSubmitJob_someTasksPermanentlyFailed() { int successfulTasks = 3; int failedTasks = 2; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(successfulTasks) - .failedTasksCount(failedTasks) - .errors(List.of("error1", "error2", "error3")) - .retries(2) - .taskProcessingTimeMs(100) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .errors(List.of("error1", "error2", "error3")) + .retries(2) + .taskProcessingTimeMs(100) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -145,24 +133,13 @@ public class JobManagerTest extends AbstractControllerTest { }); assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); }); - - checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job task failed"); - assertThat(notification.getText()).isEqualTo("Test job failed: 3/5 successful, 2 failed"); - }); } @Test public void testSubmitJob_taskTimeout() { - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(1) - .taskProcessingTimeMs(5000) // bigger than DummyTaskProcessor.getTaskProcessingTimeout() - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(1) + .taskProcessingTimeMs(5000) // bigger than DummyTaskProcessor.getTaskProcessingTimeout() .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -177,15 +154,9 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testCancelJob_whileRunning() throws Exception { int tasksCount = 100; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(100) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(100) .build()).getId(); Thread.sleep(500); @@ -203,15 +174,9 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testCancelJob_simulateTaskProcessorRestart() throws Exception { int tasksCount = 10; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(500) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(500) .build()).getId(); // simulate cancelled jobs are forgotten @@ -239,16 +204,10 @@ public class JobManagerTest extends AbstractControllerTest { loginSysAdmin(); createDifferentTenant(); - TenantId tenantId = this.differentTenantId; - jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(1000) - .taskProcessingTimeMs(500) - .build()) + this.tenantId = this.differentTenantId; + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(1000) + .taskProcessingTimeMs(500) .build()); Thread.sleep(2000); @@ -261,25 +220,18 @@ public class JobManagerTest extends AbstractControllerTest { } @Test - public void testSubmitMultipleJobs() { + public void testSubmitMultipleJobs() throws Exception { int tasksCount = 3; int jobsCount = 3; for (int i = 1; i <= jobsCount; i++) { - Job job = Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job-" + i) - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(1000) - .build()) - .build(); - jobManager.submitJob(job); + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) + .build(), "test-job-" + i); } await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - List jobs = findJobs(JobType.DUMMY); + List jobs = findJobs(List.of(JobType.DUMMY), List.of(jobEntity.getUuidId())); assertThat(jobs).hasSize(jobsCount); Job firstJob = jobs.get(2); // ordered by createdTime descending assertThat(firstJob.getStatus()).isEqualTo(JobStatus.RUNNING); @@ -297,6 +249,11 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); } }); + + doDelete("/api/device/" + jobEntity.getId()).andExpect(status().isOk()); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(findJobs(List.of(JobType.DUMMY), List.of(jobEntity.getUuidId()))).isEmpty(); + }); } @Test @@ -305,17 +262,11 @@ public class JobManagerTest extends AbstractControllerTest { int jobsCount = 3; List jobIds = new ArrayList<>(); for (int i = 1; i <= jobsCount; i++) { - Job job = Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job-" + i) - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(1000) - .build()) - .build(); - jobIds.add(jobManager.submitJob(job).getId()); + Job job = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(1000) + .build(), "test-job-" + i); + jobIds.add(job.getId()); } for (int i = 1; i < jobIds.size(); i++) { @@ -343,16 +294,10 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testSubmitJob_generalError() { int submittedTasks = 100; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .generalError("Some error while submitting tasks") - .submittedTasksBeforeGeneralError(submittedTasks) - .taskProcessingTimeMs(10) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(submittedTasks) + .taskProcessingTimeMs(10) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -362,24 +307,13 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getDiscardedCount()).isZero(); assertThat(job.getResult().getTotalCount()).isNull(); }); - - checkJobNotification(notification -> { - assertThat(notification.getSubject()).isEqualTo("Dummy job task failed"); - assertThat(notification.getText()).isEqualTo("Test job failed: Some error while submitting tasks"); - }); } @Test public void testSubmitJob_immediateGeneralError() { - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .generalError("Some error while submitting tasks") - .submittedTasksBeforeGeneralError(0) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(0) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -395,16 +329,10 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testReprocessJob_generalError() throws Exception { int submittedTasks = 100; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("Test job") - .configuration(DummyJobConfiguration.builder() - .generalError("Some error while submitting tasks") - .submittedTasksBeforeGeneralError(submittedTasks) - .taskProcessingTimeMs(10) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .generalError("Some error while submitting tasks") + .submittedTasksBeforeGeneralError(submittedTasks) + .taskProcessingTimeMs(10) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -437,17 +365,11 @@ public class JobManagerTest extends AbstractControllerTest { int successfulTasks = 3; int failedTasks = 2; int totalTasksCount = successfulTasks + failedTasks; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(successfulTasks) - .failedTasksCount(failedTasks) - .errors(List.of("error")) - .taskProcessingTimeMs(100) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .errors(List.of("error")) + .taskProcessingTimeMs(100) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -484,18 +406,12 @@ public class JobManagerTest extends AbstractControllerTest { int failedTasks = 2; int permanentlyFailedTasks = 1; int totalTasksCount = successfulTasks + failedTasks + permanentlyFailedTasks; - JobId jobId = jobManager.submitJob(Job.builder() - .tenantId(tenantId) - .type(JobType.DUMMY) - .key("test-job") - .description("test job") - .configuration(DummyJobConfiguration.builder() - .successfulTasksCount(successfulTasks) - .failedTasksCount(failedTasks) - .permanentlyFailedTasksCount(permanentlyFailedTasks) - .errors(List.of("error")) - .taskProcessingTimeMs(100) - .build()) + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(successfulTasks) + .failedTasksCount(failedTasks) + .permanentlyFailedTasksCount(permanentlyFailedTasks) + .errors(List.of("error")) + .taskProcessingTimeMs(100) .build()).getId(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { @@ -534,14 +450,18 @@ public class JobManagerTest extends AbstractControllerTest { }); } - private void checkJobNotification(ThrowingConsumer assertFunction) { - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - Notification notification = getMyNotifications(true, 100).stream() - .findFirst().orElse(null); - assertThat(notification).isNotNull(); + private Job submitJob(DummyJobConfiguration configuration) { + return submitJob(configuration, "test-job"); + } - assertFunction.accept(notification); - }); + private Job submitJob(DummyJobConfiguration configuration, String key) { + return jobManager.submitJob(Job.builder() + .tenantId(tenantId) + .type(JobType.DUMMY) + .key(key) + .entityId(jobEntity.getId()) + .configuration(configuration) + .build()); } private List getFailures(JobResult jobResult) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java index 7d4c68f6a4..4f3f9548d8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.job; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -42,4 +43,6 @@ public interface JobService extends EntityDaoService { void deleteJob(TenantId tenantId, JobId jobId); + int deleteJobsByEntityId(TenantId tenantId, EntityId entityId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index ed02d22a74..2d88cef037 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -85,6 +85,10 @@ public class HousekeeperTask implements Serializable { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_CALCULATED_FIELDS); } + public static HousekeeperTask deleteJobs(TenantId tenantId, EntityId entityId) { + return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_JOBS); + } + @JsonIgnore public String getDescription() { return taskType.getDescription() + " for " + entityId.getEntityType().getNormalName().toLowerCase() + " " + entityId.getId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java index ef217debc3..e84642b599 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java @@ -31,7 +31,8 @@ public enum HousekeeperTaskType { UNASSIGN_ALARMS("alarms unassigning"), DELETE_TENANT_ENTITIES("tenant entities deletion"), DELETE_ENTITIES("entities deletion"), - DELETE_CALCULATED_FIELDS("calculated fields deletion"); + DELETE_CALCULATED_FIELDS("calculated fields deletion"), + DELETE_JOBS("jobs deletion"); private final String description; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java index 3a9aabae76..031a733d51 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java @@ -17,14 +17,6 @@ package org.thingsboard.server.common.data.job; public class DummyJobResult extends JobResult { - @Override - public String getDescription() { - if (getGeneralError() != null) { - return getGeneralError(); - } - return getSuccessfulCount() + "/" + getTotalCount() + " successful, " + getFailedCount() + " failed"; - } - @Override public JobType getJobType() { return JobType.DUMMY; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index f914067be3..3cfb55b388 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -24,10 +24,13 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Set; import java.util.UUID; @Data @@ -42,8 +45,8 @@ public class Job extends BaseData implements HasTenantId { private JobType type; @NotBlank private String key; - @NotBlank - private String description; + @NotNull + private EntityId entityId; @NotNull private JobStatus status; @NotNull @@ -52,12 +55,16 @@ public class Job extends BaseData implements HasTenantId { @NotNull private JobResult result; + public static final Set SUPPORTED_ENTITY_TYPES = Set.of( + EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE + ); + @Builder(toBuilder = true) - public Job(TenantId tenantId, JobType type, String key, String description, JobConfiguration configuration) { + public Job(TenantId tenantId, JobType type, String key, EntityId entityId, JobConfiguration configuration) { this.tenantId = tenantId; this.type = type; this.key = key; - this.description = description; + this.entityId = entityId; this.configuration = configuration; this.configuration.setTasksKey(UUID.randomUUID().toString()); presetResult(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java index 6cc9a636e8..1a678ba34b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java @@ -19,6 +19,7 @@ import lombok.Builder; import lombok.Data; import java.util.List; +import java.util.UUID; @Data @Builder @@ -26,5 +27,8 @@ public class JobFilter { private final List types; private final List statuses; + private final List entities; + private final Long startTime; + private final Long endTime; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 285143dfa4..bd2c73d0d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -66,9 +66,6 @@ public abstract class JobResult implements Serializable { } } - @JsonIgnore - public abstract String getDescription(); - @JsonIgnore public abstract JobType getJobType(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 1ca2973936..0340cd0fde 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.eventsourcing.ActionCause; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; @@ -76,6 +77,9 @@ public class CleanUpService { submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); submitTask(HousekeeperTask.deleteAlarms(tenantId, entityId)); submitTask(HousekeeperTask.deleteCalculatedFields(tenantId, entityId)); + if (Job.SUPPORTED_ENTITY_TYPES.contains(entityId.getEntityType())) { + submitTask(HousekeeperTask.deleteJobs(tenantId, entityId)); + } } public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index cd001b61b4..5afac21e5a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -157,6 +157,10 @@ public class DefaultJobService extends AbstractEntityService implements JobServi private Job saveJob(TenantId tenantId, Job job, boolean publishEvent, JobStatus prevStatus) { ConstraintValidator.validateFields(job); + if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) { + throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType()); + } + job = jobDao.save(tenantId, job); if (publishEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder() @@ -203,6 +207,11 @@ public class DefaultJobService extends AbstractEntityService implements JobServi jobDao.removeById(tenantId, jobId.getId()); } + @Override + public int deleteJobsByEntityId(TenantId tenantId, EntityId entityId) { // TODO: cancel all jobs for this entity + return jobDao.removeByEntityId(tenantId, entityId); + } + private Job findForUpdate(TenantId tenantId, JobId jobId) { return jobDao.findByIdForUpdate(tenantId, jobId); } @@ -219,7 +228,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public void deleteByTenantId(TenantId tenantId) { - jobDao.deleteByTenantId(tenantId); + jobDao.removeByTenantId(tenantId); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java index 46cc70aea8..498fc9f2de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/JobDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.job; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -37,8 +38,12 @@ public interface JobDao extends Dao { boolean existsByTenantIdAndTypeAndStatusOneOf(TenantId tenantId, JobType type, JobStatus... statuses); + boolean existsByTenantIdAndEntityIdAndStatusOneOf(TenantId tenantId, EntityId entityId, JobStatus... statuses); + Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status); - void deleteByTenantId(TenantId tenantId); + void removeByTenantId(TenantId tenantId); + + int removeByEntityId(TenantId tenantId, EntityId entityId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 707dcdc3a5..23324eccb5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -745,7 +745,8 @@ public class ModelConstants { public static final String JOB_TABLE_NAME = "job"; public static final String JOB_TYPE_PROPERTY = "type"; public static final String JOB_KEY_PROPERTY = "key"; - public static final String JOB_DESCRIPTION_PROPERTY = "description"; + public static final String JOB_ENTITY_ID_PROPERTY = "entity_id"; + public static final String JOB_ENTITY_TYPE_PROPERTY = "entity_type"; public static final String JOB_STATUS_PROPERTY = "status"; public static final String JOB_CONFIGURATION_PROPERTY = "configuration"; public static final String JOB_RESULT_PROPERTY = "result"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java index 498541962b..16d7d33edc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/JobEntity.java @@ -25,6 +25,8 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobConfiguration; @@ -54,8 +56,12 @@ public class JobEntity extends BaseSqlEntity { @Column(name = ModelConstants.JOB_KEY_PROPERTY, nullable = false) private String key; - @Column(name = ModelConstants.JOB_DESCRIPTION_PROPERTY, nullable = false) - private String description; + @Column(name = ModelConstants.JOB_ENTITY_ID_PROPERTY, nullable = false) + private UUID entityId; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.JOB_ENTITY_TYPE_PROPERTY, nullable = false) + private EntityType entityType; @Enumerated(EnumType.STRING) @Column(name = ModelConstants.JOB_STATUS_PROPERTY, nullable = false) @@ -74,7 +80,8 @@ public class JobEntity extends BaseSqlEntity { this.tenantId = getTenantUuid(job.getTenantId()); this.type = job.getType(); this.key = job.getKey(); - this.description = job.getDescription(); + this.entityId = job.getEntityId().getId(); + this.entityType = job.getEntityId().getEntityType(); this.status = job.getStatus(); this.configuration = toJson(job.getConfiguration()); this.result = toJson(job.getResult()); @@ -88,7 +95,7 @@ public class JobEntity extends BaseSqlEntity { job.setTenantId(getTenantId(tenantId)); job.setType(type); job.setKey(key); - job.setDescription(description); + job.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); job.setStatus(status); job.setConfiguration(fromJson(configuration, JobConfiguration.class)); job.setResult(fromJson(result, JobResult.class)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java index 72d569c94b..df391be1b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JobRepository.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.job; +import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -34,26 +35,34 @@ import java.util.UUID; public interface JobRepository extends JpaRepository { @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId " + - "AND (:types IS NULL OR j.type IN (:types)) AND (:statuses IS NULL OR j.status IN (:statuses)) " + - "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true " + - "OR ilike(j.description, concat('%', :searchText, '%')) = true)") - Page findByTenantIdAndTypesAndStatusesAndSearchText(@Param("tenantId") UUID tenantId, - @Param("types") List types, - @Param("statuses") List statuses, - @Param("searchText") String searchText, - Pageable pageable); + "AND (:types IS NULL OR j.type IN (:types)) " + + "AND (:statuses IS NULL OR j.status IN (:statuses)) " + + "AND (:entities IS NULL OR j.entityId IN :entities) " + + "AND (:startTime <= 0 OR j.createdTime >= :startTime) " + + "AND (:endTime <= 0 OR j.createdTime <= :endTime) " + + "AND (:searchText IS NULL OR ilike(j.key, concat('%', :searchText, '%')) = true)") + Page findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(@Param("tenantId") UUID tenantId, + @Param("types") List types, + @Param("statuses") List statuses, + @Param("entities") List entities, + @Param("startTime") long startTime, + @Param("endTime") long endTime, + @Param("searchText") String searchText, + Pageable pageable); @Query(value = "SELECT * FROM job j WHERE j.id = :id FOR UPDATE", nativeQuery = true) JobEntity findByIdForUpdate(UUID id); @Query("SELECT j FROM JobEntity j WHERE j.tenantId = :tenantId AND j.key = :key " + "ORDER BY j.createdTime DESC") - JobEntity findLatestByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key); + JobEntity findLatestByTenantIdAndKey(@Param("tenantId") UUID tenantId, @Param("key") String key, Limit limit); boolean existsByTenantIdAndKeyAndStatusIn(UUID tenantId, String key, List statuses); boolean existsByTenantIdAndTypeAndStatusIn(UUID tenantId, JobType type, List statuses); + boolean existsByTenantIdAndEntityIdAndStatusIn(UUID tenantId, UUID entityId, List statuses); + @Query(value = "SELECT * FROM job j WHERE j.tenant_id = :tenantId AND j.type = :type " + "AND j.status = :status ORDER BY j.created_time ASC, j.id ASC LIMIT 1 FOR UPDATE", nativeQuery = true) JobEntity findOldestByTenantIdAndTypeAndStatusForUpdate(UUID tenantId, String type, String status); @@ -63,4 +72,9 @@ public interface JobRepository extends JpaRepository { @Query("DELETE FROM JobEntity j WHERE j.tenantId = :tenantId") void deleteByTenantId(UUID tenantId); + @Transactional + @Modifying + @Query("DELETE FROM JobEntity j WHERE j.entityId = :entityId") + int deleteByEntityId(UUID entityId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 5c9fe230e8..40d5177ad6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.job; import com.google.common.base.Strings; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; @@ -47,9 +49,12 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao @Override public PageData findByTenantIdAndFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { - return DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndSearchText(tenantId.getId(), + return DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(tenantId.getId(), CollectionsUtil.isEmpty(filter.getTypes()) ? null : filter.getTypes(), CollectionsUtil.isEmpty(filter.getStatuses()) ? null : filter.getStatuses(), + CollectionsUtil.isEmpty(filter.getEntities()) ? null : filter.getEntities(), + filter.getStartTime() != null ? filter.getStartTime() : 0, + filter.getEndTime() != null ? filter.getEndTime() : 0, Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); } @@ -60,7 +65,7 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao @Override public Job findLatestByTenantIdAndKey(TenantId tenantId, String key) { - return DaoUtil.getData(jobRepository.findLatestByTenantIdAndKey(tenantId.getId(), key)); + return DaoUtil.getData(jobRepository.findLatestByTenantIdAndKey(tenantId.getId(), key, Limit.of(1))); } @Override @@ -73,16 +78,26 @@ public class JpaJobDao extends JpaAbstractDao implements JobDao return jobRepository.existsByTenantIdAndTypeAndStatusIn(tenantId.getId(), type, Arrays.stream(statuses).toList()); } + @Override + public boolean existsByTenantIdAndEntityIdAndStatusOneOf(TenantId tenantId, EntityId entityId, JobStatus... statuses) { + return jobRepository.existsByTenantIdAndEntityIdAndStatusIn(tenantId.getId(), entityId.getId(), Arrays.stream(statuses).toList()); + } + @Override public Job findOldestByTenantIdAndTypeAndStatusForUpdate(TenantId tenantId, JobType type, JobStatus status) { return DaoUtil.getData(jobRepository.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId.getId(), type.name(), status.name())); } @Override - public void deleteByTenantId(TenantId tenantId) { + public void removeByTenantId(TenantId tenantId) { jobRepository.deleteByTenantId(tenantId.getId()); } + @Override + public int removeByEntityId(TenantId tenantId, EntityId entityId) { + return jobRepository.deleteByEntityId(entityId.getId()); + } + @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 62ce86a76c..f2a0bc26c1 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -955,7 +955,8 @@ CREATE TABLE IF NOT EXISTS job ( tenant_id uuid NOT NULL, type varchar NOT NULL, key varchar NOT NULL, - description varchar NOT NULL, + entity_id uuid NOT NULL, + entity_type varchar NOT NULL, status varchar NOT NULL, configuration varchar NOT NULL, result varchar diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index 3ee29dd7c0..89434b20cf 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.job.Job; public interface JobManager { - Job submitJob(Job job); + Job submitJob(Job job); // TODO: rate limits void cancelJob(TenantId tenantId, JobId jobId); From 2bb6eab0edbfa0c116a86f327f7297f05cbb4f14 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 May 2025 14:30:04 +0300 Subject: [PATCH 40/44] Make submitJob async --- .../thingsboard/server/service/job/DefaultJobManager.java | 5 +++-- .../org/thingsboard/server/service/job/JobManagerTest.java | 4 +++- .../java/org/thingsboard/rule/engine/api/JobManager.java | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 379e72d28b..2ed8ed8a42 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.function.Function; import java.util.stream.Collectors; @@ -77,9 +78,9 @@ public class DefaultJobManager implements JobManager { } @Override - public Job submitJob(Job job) { + public Future submitJob(Job job) { log.debug("Submitting job: {}", job); - return jobService.saveJob(job.getTenantId(), job); + return executor.submit(() -> jobService.saveJob(job.getTenantId(), job)); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 14c0e4e847..0243d119b1 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.job; +import lombok.SneakyThrows; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -454,6 +455,7 @@ public class JobManagerTest extends AbstractControllerTest { return submitJob(configuration, "test-job"); } + @SneakyThrows private Job submitJob(DummyJobConfiguration configuration, String key) { return jobManager.submitJob(Job.builder() .tenantId(tenantId) @@ -461,7 +463,7 @@ public class JobManagerTest extends AbstractControllerTest { .key(key) .entityId(jobEntity.getId()) .configuration(configuration) - .build()); + .build()).get(); } private List getFailures(JobResult jobResult) { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index 89434b20cf..aa48f9a9fb 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -19,9 +19,11 @@ import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import java.util.concurrent.Future; + public interface JobManager { - Job submitJob(Job job); // TODO: rate limits + Future submitJob(Job job); // TODO: rate limits void cancelJob(TenantId tenantId, JobId jobId); From 43176d37fc8cc5e97b2d8187d8a013465a9019c1 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 23 May 2025 15:20:08 +0300 Subject: [PATCH 41/44] Add entity name for jobs --- .../server/service/job/DefaultJobManager.java | 7 ++-- .../server/service/job/JobManagerTest.java | 2 ++ .../server/dao/entity/EntityService.java | 5 +++ .../server/common/data/job/Job.java | 1 + .../server/dao/entity/BaseEntityService.java | 32 ++++++++++++++++++- .../server/dao/sql/job/JpaJobDao.java | 20 +++++++++++- .../rule/engine/api/JobManager.java | 5 ++- 7 files changed, 64 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index 2ed8ed8a42..ed8e613f38 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.job; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -49,7 +51,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.function.Function; import java.util.stream.Collectors; @@ -78,9 +79,9 @@ public class DefaultJobManager implements JobManager { } @Override - public Future submitJob(Job job) { + public ListenableFuture submitJob(Job job) { log.debug("Submitting job: {}", job); - return executor.submit(() -> jobService.saveJob(job.getTenantId(), job)); + return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 0243d119b1..0278e910d9 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -248,6 +248,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); + assertThat(job.getEntityId()).isEqualTo(jobEntity.getId()); + assertThat(job.getEntityName()).isEqualTo(jobEntity.getName()); } }); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 65db0d5a76..9adf703e0b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -25,7 +26,9 @@ import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; +import java.util.Map; import java.util.Optional; +import java.util.Set; public interface EntityService { @@ -37,6 +40,8 @@ public interface EntityService { Optional> fetchEntity(TenantId tenantId, EntityId entityId); + Map fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set entityIds); + Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId); long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index 3cfb55b388..4af60bbd5e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -47,6 +47,7 @@ public class Job extends BaseData implements HasTenantId { private String key; @NotNull private EntityId entityId; + private String entityName; // read-only @NotNull private JobStatus status; @NotNull diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index be7cbf7f84..d7e313e7fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasEmail; @@ -41,19 +42,24 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilterType; import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityNameFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -199,6 +205,30 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return fetchAndConvert(tenantId, entityId, Function.identity()); } + @Override + public Map fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set entityIds) { + Map infos = new HashMap<>(); + entityIds.stream() + .collect(Collectors.groupingBy(EntityId::getEntityType)) + .forEach((entityType, ids) -> { + EntityListFilter filter = new EntityListFilter(); + filter.setEntityType(entityType); + filter.setEntityList(ids.stream().map(Object::toString).toList()); + EntityDataQuery query = new EntityDataQuery(filter, new EntityDataPageLink(ids.size(), 0, null, null), + List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.NAME_PROPERTY)), Collections.emptyList(), Collections.emptyList()); + + entityQueryDao.findEntityDataByQuery(tenantId, customerId, query).getData().forEach(entityData -> { + EntityId entityId = entityData.getEntityId(); + Optional.ofNullable(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD)) + .map(fields -> fields.get(ModelConstants.NAME_PROPERTY)) + .map(TsValue::getValue).ifPresent(name -> { + infos.put(entityId, new EntityInfo(entityId, name)); + }); + }); + }); + return infos; + } + private Optional fetchAndConvert(TenantId tenantId, EntityId entityId, Function, T> converter) { EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); Optional> entityOpt = entityDaoService.findEntity(tenantId, entityId); @@ -295,7 +325,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && - (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { + (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { return false; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 40d5177ad6..339f6af033 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; @@ -32,13 +33,17 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.model.sql.JobEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Arrays; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Component @SqlDao @@ -46,16 +51,29 @@ import java.util.UUID; public class JpaJobDao extends JpaAbstractDao implements JobDao { private final JobRepository jobRepository; + private final EntityService entityService; @Override public PageData findByTenantIdAndFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { - return DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(tenantId.getId(), + PageData jobs = DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(tenantId.getId(), CollectionsUtil.isEmpty(filter.getTypes()) ? null : filter.getTypes(), CollectionsUtil.isEmpty(filter.getStatuses()) ? null : filter.getStatuses(), CollectionsUtil.isEmpty(filter.getEntities()) ? null : filter.getEntities(), filter.getStartTime() != null ? filter.getStartTime() : 0, filter.getEndTime() != null ? filter.getEndTime() : 0, Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); + + Set entityIds = jobs.getData().stream() + .map(Job::getEntityId) + .collect(Collectors.toSet()); + Map entityInfos = entityService.fetchEntityInfos(tenantId, null, entityIds); + jobs.getData().forEach(job -> { + EntityInfo entityInfo = entityInfos.get(job.getEntityId()); + if (entityInfo != null) { + job.setEntityName(entityInfo.getName()); + } + }); + return jobs; } @Override diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index aa48f9a9fb..ed8931f88e 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -15,15 +15,14 @@ */ package org.thingsboard.rule.engine.api; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; -import java.util.concurrent.Future; - public interface JobManager { - Future submitJob(Job job); // TODO: rate limits + ListenableFuture submitJob(Job job); // TODO: rate limits void cancelJob(TenantId tenantId, JobId jobId); From e5d6b2bf1b02216bbb8f0f21d763871b5497e413 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 27 May 2025 11:31:34 +0300 Subject: [PATCH 42/44] UI: Updated shared component --- .../entity/entities-table.component.ts | 34 ++---------- .../entity/entity-list-select.component.html | 4 ++ .../entity/entity-list-select.component.ts | 52 +++++++++---------- .../entity/entity-list.component.html | 15 ++++-- .../entity/entity-list.component.ts | 27 ++++------ .../entity/entity-type-select.component.html | 8 +-- .../entity/entity-type-select.component.ts | 15 ++++-- .../time/timewindow-panel.component.html | 6 +-- .../time/timewindow-panel.component.ts | 34 ++++++++++-- .../components/time/timewindow.component.html | 9 ++-- .../components/time/timewindow.component.ts | 46 +++++++++++++++- .../src/app/shared/models/time/time.models.ts | 25 +++++++++ 12 files changed, 176 insertions(+), 99 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 4be96f5fdd..eeb835e63f 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -21,7 +21,8 @@ import { Component, ElementRef, EventEmitter, - Input, NgZone, + Input, + NgZone, OnChanges, OnDestroy, OnInit, @@ -59,7 +60,7 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; +import { getTimePageLinkInterval, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isEqual, isNotEmptyStr, isUndefined } from '@core/utils'; @@ -259,7 +260,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa if (this.entitiesTableConfig.useTimePageLink) { this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; - const interval = this.getTimePageLinkInterval(); + const interval = getTimePageLinkInterval(this.timewindow); this.pageLink = new TimePageLink(10, 0, null, sortOrder, interval.startTime, interval.endTime); } else { @@ -424,7 +425,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } if (this.entitiesTableConfig.useTimePageLink) { const timePageLink = this.pageLink as TimePageLink; - const interval = this.getTimePageLinkInterval(); + const interval = getTimePageLinkInterval(this.timewindow); timePageLink.startTime = interval.startTime; timePageLink.endTime = interval.endTime; } @@ -434,31 +435,6 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } - private getTimePageLinkInterval(): {startTime?: number; endTime?: number} { - const interval: {startTime?: number; endTime?: number} = {}; - switch (this.timewindow.history.historyType) { - case HistoryWindowType.LAST_INTERVAL: - const currentTime = Date.now(); - interval.startTime = currentTime - this.timewindow.history.timewindowMs; - interval.endTime = currentTime; - break; - case HistoryWindowType.FIXED: - interval.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; - interval.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; - break; - case HistoryWindowType.INTERVAL: - const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval); - interval.startTime = startEndTime[0]; - interval.endTime = startEndTime[1]; - break; - case HistoryWindowType.FOR_ALL_TIME: - interval.startTime = null; - interval.endTime = null; - break; - } - return interval; - } - private dataLoaded(col?: number, row?: number) { if (isFinite(col) && isFinite(row)) { this.clearCellCache(col, row); diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index 1701e8d754..39c2da9c2f 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -17,16 +17,20 @@ -->
{ }; + private propagateChange = (_v: any) => { }; - constructor(private store: Store, - private entityService: EntityService, - public translate: TranslateService, + constructor(private entityService: EntityService, private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { @@ -96,7 +94,7 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit, this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } ngOnInit() { @@ -114,9 +112,9 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit, this.updateView(this.modelValue.entityType, values); } ); - } - - ngAfterViewInit(): void { + if (isDefinedAndNotNull(this.predefinedEntityType)) { + this.defaultEntityType = this.predefinedEntityType; + } } setDisabledState(isDisabled: boolean): void { @@ -145,7 +143,7 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit, this.entityListSelectFormGroup.get('entityIds').patchValue([...this.modelValue.ids], {emitEvent: true}); } - updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array | null) { + private updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array | null) { if (this.modelValue.entityType !== entityType || !this.compareIds(this.modelValue.ids, entityIds)) { this.modelValue = { @@ -156,7 +154,7 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit, } } - compareIds(ids1: Array | null, ids2: Array | null): boolean { + private compareIds(ids1: Array | null, ids2: Array | null): boolean { if (ids1 !== null && ids2 !== null) { return JSON.stringify(ids1) === JSON.stringify(ids2); } else { @@ -164,7 +162,7 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit, } } - toEntityIds(modelValue: EntityListSelectModel): Array { + private toEntityIds(modelValue: EntityListSelectModel): Array { if (modelValue !== null && modelValue.entityType && modelValue.ids && modelValue.ids.length > 0) { const entityType = modelValue.entityType; return modelValue.ids.map(id => ({entityType, id})); diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index fc0cfa9586..e1b951b40b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -15,8 +15,13 @@ limitations under the License. --> - - {{ labelText }} + + {{ labelText }} {{ labelText }} - {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} + {{ 'entity.no-entities-matching' | translate: {entity: searchText} }}
- + {{ hint }} - + {{ requiredText }}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 5cd19156a5..552c4f1f71 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -14,17 +14,7 @@ /// limitations under the License. /// -import { - AfterViewInit, - Component, - ElementRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges, - ViewChild -} from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -65,7 +55,7 @@ import { isArray } from 'lodash'; } ] }) -export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges { +export class EntityListComponent implements ControlValueAccessor, OnInit, OnChanges { entityListFormGroup: UntypedFormGroup; @@ -115,6 +105,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @coerceBoolean() syncIdsWithDB = false; + @Input() + @coerceBoolean() + inlineField: boolean; + @ViewChild('entityInput') entityInput: ElementRef; @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; @@ -126,9 +120,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV private dirty = false; - private propagateChange = (v: any) => { }; + private propagateChange = (_v: any) => { }; - constructor(public translate: TranslateService, + constructor(private translate: TranslateService, private entityService: EntityService, private fb: UntypedFormBuilder) { this.entityListFormGroup = this.fb.group({ @@ -146,7 +140,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } ngOnInit() { @@ -178,9 +172,6 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV } } - ngAfterViewInit(): void { - } - setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html index b21734cfdd..6506d233b0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html @@ -15,14 +15,16 @@ limitations under the License. --> - - {{ 'entity.type' | translate }} + + {{ label }} {{ displayEntityTypeFn(type) }} - + {{ 'entity.type-required' | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index ce599a0c08..f8a4f49a82 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -52,6 +52,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @coerceBoolean() showLabel: boolean; + @Input() + label = this.translate.instant('entity.type'); + @Input() @coerceBoolean() required: boolean; @@ -65,12 +68,16 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @Input() appearance: MatFormFieldAppearance = 'fill'; + @Input() + @coerceBoolean() + inlineField: boolean; + entityTypes: Array; - private propagateChange = (v: any) => { }; + private propagateChange = (_v: any) => { }; constructor(private entityService: EntityService, - public translate: TranslateService, + private translate: TranslateService, private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { this.entityTypeFormGroup = this.fb.group({ @@ -82,7 +89,7 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } ngOnInit() { @@ -97,7 +104,7 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, takeUntilDestroyed(this.destroyRef) ).subscribe( (value) => { - let modelValue; + let modelValue: EntityType | AliasEntityType; if (!value || value === '') { modelValue = null; } else { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 042d94087c..fbb3c1d2f6 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -27,7 +27,7 @@
-
+
- -
+ +
- - -
{{ computedTimewindowStyle.icon }}
+ diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index ea3f49cf9f..a52e3eb091 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -17,6 +17,7 @@ import { ChangeDetectorRef, Component, + DestroyRef, ElementRef, forwardRef, HostBinding, @@ -26,6 +27,7 @@ import { OnInit, SimpleChanges, StaticProvider, + ViewChild, ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -63,6 +65,7 @@ import { } from '@shared/models/widget-settings.models'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { fromEvent } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; // @dynamic @Component({ @@ -79,6 +82,8 @@ import { fromEvent } from 'rxjs'; }) export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChanges { + @ViewChild('panelContainer', { read: ViewContainerRef, static: true }) panelContainer: ViewContainerRef; + historyOnlyValue = false; @Input() @@ -180,6 +185,10 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() disabled: boolean; + @Input() + @coerceBoolean() + panelMode = true; + innerValue: Timewindow; timewindowDisabled: boolean; @@ -197,7 +206,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan private datePipe: DatePipe, private cd: ChangeDetectorRef, private nativeElement: ElementRef, - public viewContainerRef: ViewContainerRef) { + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef) { } ngOnInit() { @@ -249,7 +259,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, timezone: this.timezone, - isEdit: this.isEdit + isEdit: this.isEdit, + panelMode: this.panelMode, } as TimewindowPanelData }, { @@ -317,6 +328,9 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } else { this.updateDisplayValue(); } + if (!this.panelMode) { + this.createPanel(); + } } notifyChanged() { @@ -328,6 +342,9 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } updateDisplayValue() { + if (!this.panelMode) { + return + } if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { this.innerValue.displayValue = this.displayTypePrefix ? (this.translate.instant('timewindow.realtime') + ' - ') : ''; if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) { @@ -373,4 +390,29 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan ))); } + private createPanel() { + this.panelContainer.clear(); + const panelData = { + timewindow: deepClone(this.innerValue), + historyOnly: this.historyOnly, + forAllTimeEnabled: this.forAllTimeEnabled, + quickIntervalOnly: this.quickIntervalOnly, + aggregation: this.aggregation, + timezone: this.timezone, + isEdit: this.isEdit, + panelMode: this.panelMode, + } + const injector = Injector.create({ + providers: [{ provide: TIMEWINDOW_PANEL_DATA, useValue: panelData }], + parent: this.viewContainerRef.injector + }); + const componentRef = this.panelContainer.createComponent(TimewindowPanelComponent, {index: 0, injector}); + componentRef.instance.changeTimewindow.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => { + this.innerValue = value; + this.timewindowDisabled = this.isTimewindowDisabled(); + this.notifyChanged(); + }) + } } diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index c63bd8da52..acb62d9d2a 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -1406,3 +1406,28 @@ export const calculateInterval = (startTime: number, endTime: number, export const getCurrentTimeForComparison = (timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment => getCurrentTime(tz).subtract(1, timeForComparison); + +export const getTimePageLinkInterval = (timewindow: Timewindow): {startTime?: number; endTime?: number} => { + const interval: {startTime?: number; endTime?: number} = {}; + switch (timewindow.history.historyType) { + case HistoryWindowType.LAST_INTERVAL: + const currentTime = Date.now(); + interval.startTime = currentTime - timewindow.history.timewindowMs; + interval.endTime = currentTime; + break; + case HistoryWindowType.FIXED: + interval.startTime = timewindow.history.fixedTimewindow.startTimeMs; + interval.endTime = timewindow.history.fixedTimewindow.endTimeMs; + break; + case HistoryWindowType.INTERVAL: + const startEndTime = calculateIntervalStartEndTime(timewindow.history.quickInterval); + interval.startTime = startEndTime[0]; + interval.endTime = startEndTime[1]; + break; + case HistoryWindowType.FOR_ALL_TIME: + interval.startTime = null; + interval.endTime = null; + break; + } + return interval; +} From 56bbb5807fed000c099625e105e78d4fa3784eeb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 May 2025 14:21:47 +0300 Subject: [PATCH 43/44] Fix dao tests init --- .../org/thingsboard/server/controller/JobController.java | 5 ----- .../org/thingsboard/server/dao/config/JpaDaoConfig.java | 2 +- .../thingsboard/server/dao/entity/BaseEntityService.java | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/JobController.java b/application/src/main/java/org/thingsboard/server/controller/JobController.java index 55c68a461f..3f3ca5e64f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/JobController.java +++ b/application/src/main/java/org/thingsboard/server/controller/JobController.java @@ -59,7 +59,6 @@ public class JobController extends BaseController { @GetMapping("/job/{id}") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public Job getJobById(@PathVariable UUID id) throws ThingsboardException { - // todo check permissions return jobService.findJobById(getTenantId(), new JobId(id)); } @@ -80,7 +79,6 @@ public class JobController extends BaseController { @RequestParam(required = false) List entities, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime) throws ThingsboardException { - // todo check permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); JobFilter filter = JobFilter.builder() .types(types) @@ -95,21 +93,18 @@ public class JobController extends BaseController { @PostMapping("/job/{id}/cancel") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public void cancelJob(@PathVariable UUID id) throws ThingsboardException { - // todo check permissions jobManager.cancelJob(getTenantId(), new JobId(id)); } @PostMapping("/job/{id}/reprocess") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public void reprocessJob(@PathVariable UUID id) throws ThingsboardException { - // todo check permissions jobManager.reprocessJob(getTenantId(), new JobId(id)); } @DeleteMapping("/job/{id}") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") public void deleteJob(@PathVariable UUID id) throws ThingsboardException { - // todo check permissions jobService.deleteJob(getTenantId(), new JobId(id)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java index 164e4cdb3c..963a114e8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java @@ -45,7 +45,7 @@ import java.util.Objects; @Configuration @TbAutoConfiguration -@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache", "org.thingsboard.server.dao.entity"}) @EnableJpaRepositories(value = {"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.sqlts.dictionary"}, excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {EventRepository.class, AuditLogRepository.class}), bootstrapMode = BootstrapMode.LAZY) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 2fca546fc9..5df926faf2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -103,7 +103,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private EdqsApiService edqsApiService; @Autowired - private EdqsStatsService edqsStatsService; + private Optional edqsStatsService; @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { @@ -123,7 +123,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } else { result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } - edqsStatsService.reportEntityCountQuery(tenantId, query, System.nanoTime() - startNs); + edqsStatsService.ifPresent(statsService -> statsService.reportEntityCountQuery(tenantId, query, System.nanoTime() - startNs)); return result; } @@ -157,7 +157,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } } } - edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); + edqsStatsService.ifPresent(statsService -> statsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs)); return result; } From 8e6e687c5df0607d63acd024f5bafc146332a451 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 May 2025 14:53:11 +0300 Subject: [PATCH 44/44] Refactor find jobs by filter --- .../server/dao/config/JpaDaoConfig.java | 2 +- .../server/dao/entity/BaseEntityService.java | 6 +++--- .../server/dao/job/DefaultJobService.java | 20 ++++++++++++++++++- .../server/dao/sql/job/JpaJobDao.java | 20 +------------------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java index 963a114e8f..164e4cdb3c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java @@ -45,7 +45,7 @@ import java.util.Objects; @Configuration @TbAutoConfiguration -@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache", "org.thingsboard.server.dao.entity"}) +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) @EnableJpaRepositories(value = {"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.sqlts.dictionary"}, excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {EventRepository.class, AuditLogRepository.class}), bootstrapMode = BootstrapMode.LAZY) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 5df926faf2..2fca546fc9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -103,7 +103,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private EdqsApiService edqsApiService; @Autowired - private Optional edqsStatsService; + private EdqsStatsService edqsStatsService; @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { @@ -123,7 +123,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } else { result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } - edqsStatsService.ifPresent(statsService -> statsService.reportEntityCountQuery(tenantId, query, System.nanoTime() - startNs)); + edqsStatsService.reportEntityCountQuery(tenantId, query, System.nanoTime() - startNs); return result; } @@ -157,7 +157,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } } } - edqsStatsService.ifPresent(statsService -> statsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs)); + edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); return result; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 5afac21e5a..153e95a404 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -34,10 +35,14 @@ import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.ConstraintValidator; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED; import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED; @@ -52,6 +57,7 @@ import static org.thingsboard.server.common.data.job.JobStatus.RUNNING; public class DefaultJobService extends AbstractEntityService implements JobService { private final JobDao jobDao; + private final EntityService entityService; @Transactional @Override @@ -190,7 +196,19 @@ public class DefaultJobService extends AbstractEntityService implements JobServi @Override public PageData findJobsByFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { - return jobDao.findByTenantIdAndFilter(tenantId, filter, pageLink); + PageData jobs = jobDao.findByTenantIdAndFilter(tenantId, filter, pageLink); + + Set entityIds = jobs.getData().stream() + .map(Job::getEntityId) + .collect(Collectors.toSet()); + Map entityInfos = entityService.fetchEntityInfos(tenantId, null, entityIds); + jobs.getData().forEach(job -> { + EntityInfo entityInfo = entityInfos.get(job.getEntityId()); + if (entityInfo != null) { + job.setEntityName(entityInfo.getName()); + } + }); + return jobs; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java index 339f6af033..40d5177ad6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/job/JpaJobDao.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; @@ -33,17 +32,13 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.model.sql.JobEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Arrays; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; @Component @SqlDao @@ -51,29 +46,16 @@ import java.util.stream.Collectors; public class JpaJobDao extends JpaAbstractDao implements JobDao { private final JobRepository jobRepository; - private final EntityService entityService; @Override public PageData findByTenantIdAndFilter(TenantId tenantId, JobFilter filter, PageLink pageLink) { - PageData jobs = DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(tenantId.getId(), + return DaoUtil.toPageData(jobRepository.findByTenantIdAndTypesAndStatusesAndEntitiesAndTimeAndSearchText(tenantId.getId(), CollectionsUtil.isEmpty(filter.getTypes()) ? null : filter.getTypes(), CollectionsUtil.isEmpty(filter.getStatuses()) ? null : filter.getStatuses(), CollectionsUtil.isEmpty(filter.getEntities()) ? null : filter.getEntities(), filter.getStartTime() != null ? filter.getStartTime() : 0, filter.getEndTime() != null ? filter.getEndTime() : 0, Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink))); - - Set entityIds = jobs.getData().stream() - .map(Job::getEntityId) - .collect(Collectors.toSet()); - Map entityInfos = entityService.fetchEntityInfos(tenantId, null, entityIds); - jobs.getData().forEach(job -> { - EntityInfo entityInfo = entityInfos.get(job.getEntityId()); - if (entityInfo != null) { - job.setEntityName(entityInfo.getName()); - } - }); - return jobs; } @Override