|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.0 KiB |
@ -0,0 +1,117 @@ |
|||||
|
/** |
||||
|
* 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.DeleteMapping; |
||||
|
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; |
||||
|
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; |
||||
|
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.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.security.permission.Operation; |
||||
|
|
||||
|
import java.util.List; |
||||
|
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 JobManager jobManager; |
||||
|
|
||||
|
@GetMapping("/job/{id}") |
||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
||||
|
public Job getJobById(@PathVariable UUID id) throws ThingsboardException { |
||||
|
JobId jobId = new JobId(id); |
||||
|
return checkJobId(jobId, Operation.READ); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/jobs") |
||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
||||
|
public PageData<Job> 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, |
||||
|
@RequestParam(required = false) List<JobType> types, |
||||
|
@RequestParam(required = false) List<JobStatus> statuses, |
||||
|
@RequestParam(required = false) List<UUID> entities, |
||||
|
@RequestParam(required = false) Long startTime, |
||||
|
@RequestParam(required = false) Long endTime) throws ThingsboardException { |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/job/{id}/cancel") |
||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
||||
|
public void cancelJob(@PathVariable UUID id) throws ThingsboardException { |
||||
|
JobId jobId = new JobId(id); |
||||
|
checkJobId(jobId, Operation.WRITE); |
||||
|
jobManager.cancelJob(getTenantId(), jobId); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/job/{id}/reprocess") |
||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
||||
|
public void reprocessJob(@PathVariable UUID id) throws ThingsboardException { |
||||
|
JobId jobId = new JobId(id); |
||||
|
checkJobId(jobId, Operation.WRITE); |
||||
|
jobManager.reprocessJob(getTenantId(), jobId); |
||||
|
} |
||||
|
|
||||
|
@DeleteMapping("/job/{id}") |
||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
||||
|
public void deleteJob(@PathVariable UUID id) throws ThingsboardException { |
||||
|
JobId jobId = new JobId(id); |
||||
|
checkJobId(jobId, Operation.DELETE); |
||||
|
jobService.deleteJob(getTenantId(), jobId); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<HousekeeperTask> { |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,207 @@ |
|||||
|
/** |
||||
|
* 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.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; |
||||
|
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.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.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.msg.queue.ServiceType; |
||||
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.dao.job.JobService; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; |
||||
|
import org.thingsboard.server.queue.TbQueueCallback; |
||||
|
import org.thingsboard.server.queue.TbQueueMsgMetadata; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
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 java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.function.Function; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Component |
||||
|
@Slf4j |
||||
|
public class DefaultJobManager implements JobManager { |
||||
|
|
||||
|
private final JobService jobService; |
||||
|
private final JobStatsService jobStatsService; |
||||
|
private final PartitionService partitionService; |
||||
|
private final TasksQueueConfig queueConfig; |
||||
|
private final Map<JobType, JobProcessor> jobProcessors; |
||||
|
private final Map<JobType, TbQueueProducer<TbProtoQueueMsg<TaskProto>>> taskProducers; |
||||
|
private final ExecutorService executor; |
||||
|
|
||||
|
public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService, |
||||
|
TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, |
||||
|
List<JobProcessor> jobProcessors) { |
||||
|
this.jobService = jobService; |
||||
|
this.jobStatsService = jobStatsService; |
||||
|
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()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListenableFuture<Job> submitJob(Job job) { |
||||
|
log.debug("Submitting job: {}", job); |
||||
|
return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onJobUpdate(Job 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 { |
||||
|
getJobProcessor(job.getType()).onJobFinished(job); |
||||
|
} catch (Throwable e) { |
||||
|
log.error("Failed to process job update: {}", job, e); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void processJob(Job job) { |
||||
|
TenantId tenantId = job.getTenantId(); |
||||
|
JobId jobId = job.getId(); |
||||
|
try { |
||||
|
JobProcessor processor = getJobProcessor(job.getType()); |
||||
|
List<TaskResult> toReprocess = job.getConfiguration().getToReprocess(); |
||||
|
if (toReprocess == null) { |
||||
|
int tasksCount = processor.process(job, this::submitTask); |
||||
|
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); |
||||
|
jobService.markAsFailed(tenantId, jobId, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@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) { |
||||
|
job.presetResult(); |
||||
|
} else { |
||||
|
List<TaskResult> 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); |
||||
|
} |
||||
|
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)) |
||||
|
.build(); |
||||
|
|
||||
|
TbQueueProducer<TbProtoQueueMsg<TaskProto>> producer = taskProducers.get(task.getJobType()); |
||||
|
EntityId entityId = null; |
||||
|
if (queueConfig.getPartitioningStrategy().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 to {}: {}", tpi, taskProto); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
log.warn("Failed to submit task: {}", task, t); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private JobProcessor getJobProcessor(JobType jobType) { |
||||
|
return jobProcessors.get(jobType); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
private void destroy() { |
||||
|
executor.shutdownNow(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
/** |
||||
|
* 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.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.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; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.function.Consumer; |
||||
|
|
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class DummyJobProcessor implements JobProcessor { |
||||
|
|
||||
|
@Override |
||||
|
public int process(Job job, Consumer<Task<?>> 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, false)); |
||||
|
} |
||||
|
Thread.sleep(configuration.getTaskProcessingTimeMs() * (configuration.getSubmittedTasksBeforeGeneralError() / 2)); // sleeping so that some tasks are processed
|
||||
|
throw new RuntimeException(configuration.getGeneralError()); |
||||
|
} |
||||
|
|
||||
|
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 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() + configuration.getPermanentlyFailedTasksCount(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void reprocess(Job job, List<TaskResult> taskFailures, Consumer<Task<?>> 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 DummyTask createTask(Job job, DummyJobConfiguration configuration, int number, List<String> errors, boolean failAlways) { |
||||
|
return DummyTask.builder() |
||||
|
.tenantId(job.getTenantId()) |
||||
|
.jobId(job.getId()) |
||||
|
.key(configuration.getTasksKey()) |
||||
|
.retries(configuration.getRetries()) |
||||
|
.number(number) |
||||
|
.processingTimeMs(configuration.getTaskProcessingTimeMs()) |
||||
|
.errors(errors) |
||||
|
.failAlways(failAlways) |
||||
|
.processingTimeoutMs(configuration.getTaskProcessingTimeoutMs()) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public JobType getType() { |
||||
|
return JobType.DUMMY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* 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; |
||||
|
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 java.util.List; |
||||
|
import java.util.function.Consumer; |
||||
|
|
||||
|
public interface JobProcessor { |
||||
|
|
||||
|
int process(Job job, Consumer<Task<?>> taskConsumer) throws Exception; |
||||
|
|
||||
|
void reprocess(Job job, List<TaskResult> taskFailures, Consumer<Task<?>> taskConsumer) throws Exception; |
||||
|
|
||||
|
default void onJobFinished(Job job) {} |
||||
|
|
||||
|
JobType getType(); |
||||
|
|
||||
|
} |
||||
@ -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<TbProtoQueueMsg<JobStatsMsg>> 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.<TbProtoQueueMsg<JobStatsMsg>>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<TbProtoQueueMsg<JobStatsMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<JobStatsMsg>> consumer) { |
||||
|
Map<JobId, JobStats> stats = new HashMap<>(); |
||||
|
|
||||
|
for (TbProtoQueueMsg<JobStatsMsg> 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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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.service.job.task; |
||||
|
|
||||
|
import org.apache.commons.lang3.tuple.Pair; |
||||
|
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.common.data.job.task.Task; |
||||
|
import org.thingsboard.server.queue.task.TaskProcessor; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.Future; |
||||
|
|
||||
|
public class DummyTaskProcessor extends TaskProcessor<DummyTask, DummyTaskResult> { |
||||
|
|
||||
|
@Override |
||||
|
public DummyTaskResult process(DummyTask task) throws Exception { |
||||
|
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); |
||||
|
} |
||||
|
return DummyTaskResult.success(task); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long getProcessingTimeout(DummyTask task) { |
||||
|
return task.getProcessingTimeoutMs() > 0 ? task.getProcessingTimeoutMs() : 2000; |
||||
|
} |
||||
|
|
||||
|
public Map<Object, Pair<Task<DummyTaskResult>, Future<DummyTaskResult>>> getCurrentTasks() { |
||||
|
return currentTasks; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public JobType getJobType() { |
||||
|
return JobType.DUMMY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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.service.notification.rule.trigger; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.common.data.notification.info.ResourcesShortageNotificationInfo; |
||||
|
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; |
||||
|
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger; |
||||
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; |
||||
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig; |
||||
|
|
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class ResourcesShortageTriggerProcessor implements NotificationRuleTriggerProcessor<ResourcesShortageTrigger, ResourcesShortageNotificationRuleTriggerConfig> { |
||||
|
|
||||
|
@Override |
||||
|
public boolean matchesFilter(ResourcesShortageTrigger trigger, ResourcesShortageNotificationRuleTriggerConfig triggerConfig) { |
||||
|
float usagePercent = trigger.getUsage() / 100.0f; |
||||
|
return switch (trigger.getResource()) { |
||||
|
case CPU -> usagePercent >= triggerConfig.getCpuThreshold(); |
||||
|
case RAM -> usagePercent >= triggerConfig.getRamThreshold(); |
||||
|
case STORAGE -> usagePercent >= triggerConfig.getStorageThreshold(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) { |
||||
|
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public NotificationRuleTriggerType getTriggerType() { |
||||
|
return NotificationRuleTriggerType.RESOURCES_SHORTAGE; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<List<TsKvEntry>> getTimeseries(EntityId entityId, List<String> keys, Long startTs, Long endTs, IntervalType intervalType, |
||||
|
Long interval, String timeZone, Integer limit, Aggregation agg, String orderBy, |
||||
|
Boolean useStrictDataTypes, SecurityUser currentUser) { |
||||
|
SettableFuture<List<TsKvEntry>> 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<ReadTsKvQuery> 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<TsKvEntry> 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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<List<TsKvEntry>> getTimeseries(EntityId entityId, |
||||
|
List<String> keys, |
||||
|
Long startTs, |
||||
|
Long endTs, |
||||
|
IntervalType intervalType, |
||||
|
Long interval, |
||||
|
String timeZone, |
||||
|
Integer limit, |
||||
|
Aggregation agg, |
||||
|
String orderBy, |
||||
|
Boolean useStrictDataTypes, |
||||
|
SecurityUser currentUser) throws ThingsboardException; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,504 @@ |
|||||
|
/** |
||||
|
* 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.SneakyThrows; |
||||
|
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.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; |
||||
|
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.page.PageLink; |
||||
|
import org.thingsboard.server.controller.AbstractControllerTest; |
||||
|
import org.thingsboard.server.dao.job.JobDao; |
||||
|
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; |
||||
|
|
||||
|
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; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
@TestPropertySource(properties = { |
||||
|
"queue.tasks.stats.processing_interval=0" |
||||
|
}) |
||||
|
public class JobManagerTest extends AbstractControllerTest { |
||||
|
|
||||
|
@Autowired |
||||
|
private JobManager jobManager; |
||||
|
|
||||
|
@SpyBean |
||||
|
private TestTaskProcessor taskProcessor; |
||||
|
|
||||
|
@SpyBean |
||||
|
private JobStatsService jobStatsService; |
||||
|
|
||||
|
@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 |
||||
|
public void tearDown() throws Exception { |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSubmitJob_allTasksSuccessful() { |
||||
|
int tasksCount = 5; |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(tasksCount) |
||||
|
.taskProcessingTimeMs(1000) |
||||
|
.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().getResults()).isEmpty(); |
||||
|
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSubmitJob_someTasksPermanentlyFailed() { |
||||
|
int successfulTasks = 3; |
||||
|
int failedTasks = 2; |
||||
|
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(() -> { |
||||
|
Job job = findJobById(jobId); |
||||
|
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); |
||||
|
JobResult jobResult = job.getResult(); |
||||
|
assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); |
||||
|
assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks); |
||||
|
assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks); |
||||
|
assertThat(getFailures(jobResult)).hasSize(2).allSatisfy(failure -> { |
||||
|
assertThat(failure.getError()).isEqualTo("error3"); // last error
|
||||
|
}); |
||||
|
assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSubmitJob_taskTimeout() { |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(1) |
||||
|
.taskProcessingTimeMs(5000) // bigger than DummyTaskProcessor.getTaskProcessingTimeout()
|
||||
|
.build()).getId(); |
||||
|
|
||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { |
||||
|
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; |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(tasksCount) |
||||
|
.taskProcessingTimeMs(100) |
||||
|
.build()).getId(); |
||||
|
|
||||
|
Thread.sleep(500); |
||||
|
cancelJob(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().getDiscardedCount()).isBetween(1, tasksCount - 1); |
||||
|
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); |
||||
|
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCancelJob_whileTaskRunning() throws Exception { |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(1) |
||||
|
.taskProcessingTimeMs(TimeUnit.HOURS.toMillis(1)) |
||||
|
.taskProcessingTimeoutMs(TimeUnit.HOURS.toMillis(1)) |
||||
|
.build()).getId(); |
||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { |
||||
|
assertThat(taskProcessor.getCurrentTasks()).isNotEmpty(); |
||||
|
Job job = findJobById(jobId); |
||||
|
assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING); |
||||
|
assertThat(job.getResult().getTotalCount()).isEqualTo(1); |
||||
|
assertThat(job.getResult().getCompletedCount()).isZero(); |
||||
|
}); |
||||
|
|
||||
|
cancelJob(jobId); |
||||
|
|
||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { |
||||
|
Job job = findJobById(jobId); |
||||
|
assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); |
||||
|
assertThat(job.getResult().getTotalCount()).isEqualTo(1); |
||||
|
assertThat(job.getResult().getDiscardedCount()).isEqualTo(1); |
||||
|
assertThat(job.getResult().getFailedCount()).isZero(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCancelJob_simulateTaskProcessorRestart() throws Exception { |
||||
|
int tasksCount = 10; |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(tasksCount) |
||||
|
.taskProcessingTimeMs(500) |
||||
|
.build()).getId(); |
||||
|
|
||||
|
// simulate cancelled jobs are forgotten
|
||||
|
AtomicInteger cancellationRenotifyAttempt = new AtomicInteger(0); |
||||
|
doAnswer(inv -> { |
||||
|
if (cancellationRenotifyAttempt.incrementAndGet() >= 5) { |
||||
|
inv.callRealMethod(); |
||||
|
} |
||||
|
return null; |
||||
|
}).when(taskProcessor).addToDiscarded(any()); // ignoring cancellation event,
|
||||
|
cancelJob(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().getDiscardedCount()).isBetween(1, tasksCount - 1); |
||||
|
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); |
||||
|
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void whenTenantIsDeleted_thenCancelAllTheJobs() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
createDifferentTenant(); |
||||
|
|
||||
|
this.tenantId = this.differentTenantId; |
||||
|
submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(1000) |
||||
|
.taskProcessingTimeMs(500) |
||||
|
.build()); |
||||
|
|
||||
|
Thread.sleep(2000); |
||||
|
deleteDifferentTenant(); |
||||
|
Mockito.reset(jobStatsService); |
||||
|
|
||||
|
Thread.sleep(3000); |
||||
|
verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); |
||||
|
assertThat(jobDao.findByTenantIdAndFilter(tenantId, JobFilter.builder().build(), new PageLink(100)).getData()).isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSubmitMultipleJobs() throws Exception { |
||||
|
int tasksCount = 3; |
||||
|
int jobsCount = 3; |
||||
|
for (int i = 1; i <= jobsCount; i++) { |
||||
|
submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(tasksCount) |
||||
|
.taskProcessingTimeMs(1000) |
||||
|
.build(), "test-job-" + i); |
||||
|
} |
||||
|
|
||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { |
||||
|
List<Job> 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); |
||||
|
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<Job> jobs = findJobs(); |
||||
|
for (Job job : jobs) { |
||||
|
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()); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
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 |
||||
|
public void testCancelQueuedJob() throws Exception { |
||||
|
int tasksCount = 3; |
||||
|
int jobsCount = 3; |
||||
|
List<JobId> jobIds = new ArrayList<>(); |
||||
|
for (int i = 1; i <= jobsCount; i++) { |
||||
|
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++) { |
||||
|
cancelJob(jobIds.get(i)); |
||||
|
} |
||||
|
|
||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { |
||||
|
List<Job> 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 testSubmitJob_generalError() { |
||||
|
int submittedTasks = 100; |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.generalError("Some error while submitting tasks") |
||||
|
.submittedTasksBeforeGeneralError(submittedTasks) |
||||
|
.taskProcessingTimeMs(10) |
||||
|
.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()).isZero(); |
||||
|
assertThat(job.getResult().getTotalCount()).isNull(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSubmitJob_immediateGeneralError() { |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.generalError("Some error while submitting tasks") |
||||
|
.submittedTasksBeforeGeneralError(0) |
||||
|
.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 = submitJob(DummyJobConfiguration.builder() |
||||
|
.generalError("Some error while submitting tasks") |
||||
|
.submittedTasksBeforeGeneralError(submittedTasks) |
||||
|
.taskProcessingTimeMs(10) |
||||
|
.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; |
||||
|
JobId jobId = submitJob(DummyJobConfiguration.builder() |
||||
|
.successfulTasksCount(successfulTasks) |
||||
|
.failedTasksCount(failedTasks) |
||||
|
.errors(List.of("error")) |
||||
|
.taskProcessingTimeMs(100) |
||||
|
.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); |
||||
|
|
||||
|
List<DummyTaskFailure> failures = getFailures(jobResult); |
||||
|
for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { |
||||
|
DummyTaskFailure failure = failures.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().getResults()).isEmpty(); |
||||
|
assertThat(job.getConfiguration().getToReprocess()).isNullOrEmpty(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testReprocessJob_somePermanentlyFailed() throws Exception { |
||||
|
int successfulTasks = 3; |
||||
|
int failedTasks = 2; |
||||
|
int permanentlyFailedTasks = 1; |
||||
|
int totalTasksCount = successfulTasks + failedTasks + permanentlyFailedTasks; |
||||
|
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(() -> { |
||||
|
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); |
||||
|
|
||||
|
List<DummyTaskFailure> failures = getFailures(jobResult); |
||||
|
for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { |
||||
|
DummyTaskFailure failure = failures.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); |
||||
|
|
||||
|
List<DummyTaskFailure> failures = getFailures(jobResult); |
||||
|
for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { |
||||
|
DummyTaskFailure failure = failures.get(i); |
||||
|
assertThat(failure.getNumber()).isEqualTo(taskNumber); |
||||
|
assertThat(failure.getError()).isEqualTo("error"); |
||||
|
assertThat(failure.isFailAlways()).isTrue(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private Job submitJob(DummyJobConfiguration configuration) { |
||||
|
return submitJob(configuration, "test-job"); |
||||
|
} |
||||
|
|
||||
|
@SneakyThrows |
||||
|
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()).get(); |
||||
|
} |
||||
|
|
||||
|
private List<DummyTaskFailure> getFailures(JobResult jobResult) { |
||||
|
return jobResult.getResults().stream() |
||||
|
.map(taskResult -> ((DummyTaskResult) taskResult).getFailure()) |
||||
|
.sorted(Comparator.comparingInt(DummyTaskFailure::getNumber)) |
||||
|
.toList(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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=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 testSubmitJob_generalError() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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 { |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
/** |
||||
|
* 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.subscription; |
||||
|
|
||||
|
import org.apache.commons.lang3.RandomStringUtils; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.springframework.test.util.ReflectionTestUtils; |
||||
|
import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.query.EntityData; |
||||
|
import org.thingsboard.server.common.data.query.EntityKeyType; |
||||
|
import org.thingsboard.server.common.data.query.TsValue; |
||||
|
import org.thingsboard.server.service.ws.WebSocketService; |
||||
|
import org.thingsboard.server.service.ws.WebSocketSessionRef; |
||||
|
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; |
||||
|
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; |
||||
|
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.BDDMockito.then; |
||||
|
import static org.mockito.Mockito.mock; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbEntityDataSubCtxTest { |
||||
|
|
||||
|
private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
||||
|
|
||||
|
private final Integer cmdId = RandomUtils.nextInt(); |
||||
|
private final Integer subscriptionId = RandomUtils.nextInt(); |
||||
|
private final String serviceId = RandomStringUtils.randomAlphanumeric(10); |
||||
|
private final String sessionId = RandomStringUtils.randomAlphanumeric(10); |
||||
|
|
||||
|
private final int maxEntitiesPerDataSubscription = 100; |
||||
|
|
||||
|
private TbEntityDataSubCtx subCtx; |
||||
|
@Mock |
||||
|
private WebSocketService webSocketService; |
||||
|
@Mock |
||||
|
private WebSocketSessionRef webSocketSessionRef; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
when(webSocketSessionRef.getSessionId()).thenReturn(sessionId); |
||||
|
subCtx = new TbEntityDataSubCtx(serviceId, webSocketService, mock(), mock(), mock(), mock(), webSocketSessionRef, cmdId, maxEntitiesPerDataSubscription); |
||||
|
|
||||
|
Map<Integer, EntityId> subToEntityIdMap = new HashMap<>(); |
||||
|
subToEntityIdMap.put(subscriptionId, deviceId); |
||||
|
ReflectionTestUtils.setField(subCtx, "subToEntityIdMap", subToEntityIdMap); |
||||
|
|
||||
|
long now = System.currentTimeMillis(); |
||||
|
long oldTs = now - 1_000_000; |
||||
|
|
||||
|
Map<String, TsValue> latestCtxValues = new HashMap<>(); |
||||
|
latestCtxValues.put("key", new TsValue(oldTs, "15")); |
||||
|
Map<EntityKeyType, Map<String, TsValue>> latest = new HashMap<>(); |
||||
|
latest.put(EntityKeyType.TIME_SERIES, latestCtxValues); |
||||
|
|
||||
|
EntityData entityData = new EntityData(); |
||||
|
entityData.setEntityId(deviceId); |
||||
|
entityData.setLatest(latest); |
||||
|
|
||||
|
PageData<EntityData> data = new PageData<>(List.of(entityData), 1, 1, true); |
||||
|
ReflectionTestUtils.setField(subCtx, "data", data); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSendLatestWsMsg() { |
||||
|
long ts = System.currentTimeMillis(); |
||||
|
List<TsKvEntry> telemetry = List.of( |
||||
|
new BasicTsKvEntry(ts - 50000, new LongDataEntry("key", 42L), 34L), |
||||
|
new BasicTsKvEntry(ts - 20000, new LongDataEntry("key", 17L), 78L) |
||||
|
); |
||||
|
|
||||
|
TelemetrySubscriptionUpdate subUpdate = new TelemetrySubscriptionUpdate(subscriptionId, telemetry); |
||||
|
|
||||
|
subCtx.sendWsMsg(sessionId, subUpdate, EntityKeyType.TIME_SERIES, true); |
||||
|
|
||||
|
Map<EntityKeyType, Map<String, TsValue>> expectedLatest = new HashMap<>(); |
||||
|
Map<String, TsValue> expectedLatestCtxValues = new HashMap<>(); |
||||
|
expectedLatestCtxValues.put("key", new TsValue(ts - 20000, "17")); // use latest telemetry
|
||||
|
expectedLatest.put(EntityKeyType.TIME_SERIES, expectedLatestCtxValues); |
||||
|
|
||||
|
EntityData expectedEntityData = new EntityData(); |
||||
|
expectedEntityData.setEntityId(deviceId); |
||||
|
expectedEntityData.setLatest(expectedLatest); |
||||
|
|
||||
|
List<EntityData> expected = List.of(expectedEntityData); |
||||
|
|
||||
|
ArgumentCaptor<CmdUpdate> cmdUpdateCaptor = ArgumentCaptor.forClass(CmdUpdate.class); |
||||
|
then(webSocketService).should().sendUpdate(eq(sessionId), cmdUpdateCaptor.capture()); |
||||
|
CmdUpdate cmdUpdate = cmdUpdateCaptor.getValue(); |
||||
|
assertThat(cmdUpdate).isInstanceOf(EntityDataUpdate.class); |
||||
|
EntityDataUpdate entityDataUpdate = (EntityDataUpdate) cmdUpdate; |
||||
|
assertThat(entityDataUpdate.getCmdId()).isEqualTo(cmdId); |
||||
|
assertThat(entityDataUpdate.getData()).isNull(); |
||||
|
assertThat(entityDataUpdate.getUpdate()).isEqualTo(expected); |
||||
|
assertThat(entityDataUpdate.getAllowedEntities()).isEqualTo(maxEntitiesPerDataSubscription); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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.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; |
||||
|
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; |
||||
|
import org.thingsboard.server.dao.entity.EntityDaoService; |
||||
|
|
||||
|
public interface JobService extends EntityDaoService { |
||||
|
|
||||
|
Job saveJob(TenantId tenantId, Job job); |
||||
|
|
||||
|
Job findJobById(TenantId tenantId, JobId jobId); |
||||
|
|
||||
|
void cancelJob(TenantId tenantId, JobId jobId); |
||||
|
|
||||
|
void markAsFailed(TenantId tenantId, JobId jobId, String error); |
||||
|
|
||||
|
void processStats(TenantId tenantId, JobId jobId, JobStats jobStats); |
||||
|
|
||||
|
PageData<Job> findJobsByFilter(TenantId tenantId, JobFilter filter, PageLink pageLink); |
||||
|
|
||||
|
Job findLatestJobByKey(TenantId tenantId, String key); |
||||
|
|
||||
|
void deleteJob(TenantId tenantId, JobId jobId); |
||||
|
|
||||
|
int deleteJobsByEntityId(TenantId tenantId, EntityId entityId); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* 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.edqs; |
||||
|
|
||||
|
public interface EdqsObjectKey {} |
||||
@ -0,0 +1,75 @@ |
|||||
|
/** |
||||
|
* 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.edqs; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Getter; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import lombok.Setter; |
||||
|
import org.apache.commons.lang3.BooleanUtils; |
||||
|
|
||||
|
@Getter |
||||
|
@NoArgsConstructor |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class EdqsState { |
||||
|
|
||||
|
private Boolean edqsReady; |
||||
|
@Setter |
||||
|
private EdqsSyncStatus syncStatus; |
||||
|
@Setter |
||||
|
private EdqsApiMode apiMode; |
||||
|
|
||||
|
public boolean updateEdqsReady(boolean ready) { |
||||
|
boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready; |
||||
|
this.edqsReady = ready; |
||||
|
return changed; |
||||
|
} |
||||
|
|
||||
|
@JsonIgnore |
||||
|
public boolean isApiReady() { |
||||
|
return edqsReady && syncStatus == EdqsSyncStatus.FINISHED; |
||||
|
} |
||||
|
|
||||
|
@JsonIgnore |
||||
|
public boolean isApiEnabled() { |
||||
|
return apiMode != null && (apiMode == EdqsApiMode.ENABLED || apiMode == EdqsApiMode.AUTO_ENABLED); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return '[' + |
||||
|
"EDQS ready: " + edqsReady + |
||||
|
", sync status: " + syncStatus + |
||||
|
", API mode: " + apiMode + |
||||
|
']'; |
||||
|
} |
||||
|
|
||||
|
public enum EdqsSyncStatus { |
||||
|
REQUESTED, |
||||
|
STARTED, |
||||
|
FINISHED, |
||||
|
FAILED |
||||
|
} |
||||
|
|
||||
|
public enum EdqsApiMode { |
||||
|
ENABLED, |
||||
|
AUTO_ENABLED, |
||||
|
DISABLED, |
||||
|
AUTO_DISABLED |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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 = "JOB", allowableValues = "JOB") |
||||
|
@Override |
||||
|
public EntityType getEntityType() { |
||||
|
return EntityType.JOB; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
/** |
||||
|
* 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 lombok.ToString; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@Builder |
||||
|
@ToString(callSuper = true) |
||||
|
public class DummyJobConfiguration extends JobConfiguration { |
||||
|
|
||||
|
private long taskProcessingTimeMs; |
||||
|
private int successfulTasksCount; |
||||
|
private int failedTasksCount; |
||||
|
private int permanentlyFailedTasksCount; |
||||
|
private List<String> errors; |
||||
|
private int retries; |
||||
|
private long taskProcessingTimeoutMs; |
||||
|
|
||||
|
private String generalError; |
||||
|
private int submittedTasksBeforeGeneralError; |
||||
|
|
||||
|
@Override |
||||
|
public JobType getType() { |
||||
|
return JobType.DUMMY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
/** |
||||
|
* 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.Valid; |
||||
|
import jakarta.validation.constraints.NotBlank; |
||||
|
import jakarta.validation.constraints.NotNull; |
||||
|
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.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 |
||||
|
@NoArgsConstructor |
||||
|
@ToString(callSuper = true) |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class Job extends BaseData<JobId> implements HasTenantId { |
||||
|
|
||||
|
@NotNull |
||||
|
private TenantId tenantId; |
||||
|
@NotNull |
||||
|
private JobType type; |
||||
|
@NotBlank |
||||
|
private String key; |
||||
|
@NotNull |
||||
|
private EntityId entityId; |
||||
|
private String entityName; // read-only
|
||||
|
@NotNull |
||||
|
private JobStatus status; |
||||
|
@NotNull |
||||
|
@Valid |
||||
|
private JobConfiguration configuration; |
||||
|
@NotNull |
||||
|
private JobResult result; |
||||
|
|
||||
|
public static final Set<EntityType> 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, EntityId entityId, JobConfiguration configuration) { |
||||
|
this.tenantId = tenantId; |
||||
|
this.type = type; |
||||
|
this.key = key; |
||||
|
this.entityId = entityId; |
||||
|
this.configuration = configuration; |
||||
|
this.configuration.setTasksKey(UUID.randomUUID().toString()); |
||||
|
presetResult(); |
||||
|
} |
||||
|
|
||||
|
public void presetResult() { |
||||
|
this.result = switch (type) { |
||||
|
case DUMMY -> new DummyJobResult(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@SuppressWarnings("unchecked") |
||||
|
public <C extends JobConfiguration> C getConfiguration() { |
||||
|
return (C) configuration; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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 com.fasterxml.jackson.annotation.JsonIgnore; |
||||
|
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; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") |
||||
|
@JsonSubTypes({ |
||||
|
@Type(name = "DUMMY", value = DummyJobConfiguration.class), |
||||
|
}) |
||||
|
@Data |
||||
|
public abstract class JobConfiguration implements Serializable { |
||||
|
|
||||
|
@NotBlank |
||||
|
private String tasksKey; // internal
|
||||
|
private List<TaskResult> toReprocess; // internal
|
||||
|
|
||||
|
@JsonIgnore |
||||
|
public abstract JobType getType(); |
||||
|
|
||||
|
} |
||||