Support reprocessing for job with general error; job deletion; refactoring

This commit is contained in:
ViacheslavKlimov 2025-05-15 16:57:27 +03:00
parent 8dda445253
commit 0dde966082
23 changed files with 217 additions and 61 deletions

View File

@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -56,14 +57,14 @@ public class JobController extends BaseController {
private final JobManager jobManager; private final JobManager jobManager;
@GetMapping("/job/{id}") @GetMapping("/job/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public Job getJobById(@PathVariable UUID id) throws ThingsboardException { public Job getJobById(@PathVariable UUID id) throws ThingsboardException {
// todo check permissions // todo check permissions
return jobService.findJobById(getTenantId(), new JobId(id)); return jobService.findJobById(getTenantId(), new JobId(id));
} }
@GetMapping("/jobs") @GetMapping("/jobs")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public PageData<Job> getJobs(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<Job> getJobs(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -86,17 +87,24 @@ public class JobController extends BaseController {
} }
@PostMapping("/job/{id}/cancel") @PostMapping("/job/{id}/cancel")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void cancelJob(@PathVariable UUID id) throws ThingsboardException { public void cancelJob(@PathVariable UUID id) throws ThingsboardException {
// todo check permissions // todo check permissions
jobManager.cancelJob(getTenantId(), new JobId(id)); jobManager.cancelJob(getTenantId(), new JobId(id));
} }
@PostMapping("/job/{id}/reprocess") @PostMapping("/job/{id}/reprocess")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void reprocessJob(@PathVariable UUID id) throws ThingsboardException { public void reprocessJob(@PathVariable UUID id) throws ThingsboardException {
// todo check permissions // todo check permissions
jobManager.reprocessJob(getTenantId(), new JobId(id)); 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));
}
} }

View File

@ -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.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job; 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.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -305,10 +304,24 @@ public class EntityStateSourcingListener {
private void onJobUpdate(Job job) { private void onJobUpdate(Job job) {
jobManager.ifPresent(jobManager -> 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 ComponentLifecycleEvent event;
tbClusterService.broadcastEntityStateChangeEvent(job.getTenantId(), job.getId(), ComponentLifecycleEvent.STOPPED); 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) { private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) {

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.service.job;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
@ -178,26 +179,29 @@ public class DefaultJobManager implements JobManager {
JobResult result = job.getResult(); JobResult result = job.getResult();
if (result.getGeneralError() != null) { if (result.getGeneralError() != null) {
throw new IllegalArgumentException("Reprocessing not allowed since job has general error"); job.presetResult();
} } else {
List<TaskResult> taskFailures = result.getResults().stream() List<TaskResult> taskFailures = result.getResults().stream()
.filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded()) .filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded())
.toList(); .toList();
if (result.getFailedCount() > taskFailures.size()) { if (result.getFailedCount() > taskFailures.size()) {
throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")"); throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")");
} }
result.setFailedCount(0); result.setFailedCount(0);
result.setResults(result.getResults().stream() result.setResults(result.getResults().stream()
.filter(TaskResult::isSuccess) .filter(TaskResult::isSuccess)
.toList()); .toList());
job.getConfiguration().setToReprocess(taskFailures); job.getConfiguration().setToReprocess(taskFailures);
}
job.getConfiguration().setTasksKey(UUID.randomUUID().toString());
jobService.saveJob(tenantId, job); jobService.saveJob(tenantId, job);
} }
private void submitTask(Task<?> task) { 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); log.debug("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task);
TaskProto taskProto = TaskProto.newBuilder() TaskProto taskProto = TaskProto.newBuilder()
.setValue(JacksonUtil.toString(task)) .setValue(JacksonUtil.toString(task))

View File

@ -76,6 +76,7 @@ public class DummyJobProcessor implements JobProcessor {
return DummyTask.builder() return DummyTask.builder()
.tenantId(job.getTenantId()) .tenantId(job.getTenantId())
.jobId(job.getId()) .jobId(job.getId())
.key(configuration.getTasksKey())
.retries(configuration.getRetries()) .retries(configuration.getRetries())
.number(number) .number(number)
.processingTimeMs(configuration.getTaskProcessingTimeMs()) .processingTimeMs(configuration.getTaskProcessingTimeMs())

View File

@ -36,7 +36,7 @@ public class DummyTaskProcessor extends TaskProcessor<DummyTask, DummyTaskResult
String error = task.getErrors().get(task.getAttempt() - 1); String error = task.getErrors().get(task.getAttempt() - 1);
throw new RuntimeException(error); throw new RuntimeException(error);
} }
return DummyTaskResult.success(); return DummyTaskResult.success(task);
} }
@Override @Override

View File

@ -579,7 +579,8 @@ public class DefaultTbClusterService implements TbClusterService {
} }
} }
private void broadcast(ComponentLifecycleMsg msg) { @Override
public void broadcast(ComponentLifecycleMsg msg) {
ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg);
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);

View File

@ -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.notification.Notification;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.AbstractControllerTest; 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.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.task.JobStatsService; import org.thingsboard.server.queue.task.JobStatsService;
@ -69,7 +69,7 @@ public class JobManagerTest extends AbstractControllerTest {
private JobStatsService jobStatsService; private JobStatsService jobStatsService;
@Autowired @Autowired
private JobService jobService; private JobDao jobDao;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -220,7 +220,7 @@ public class JobManagerTest extends AbstractControllerTest {
inv.callRealMethod(); inv.callRealMethod();
} }
return null; return null;
}).when(taskProcessor).addToDiscardedJobs(any()); // ignoring cancellation event, }).when(taskProcessor).addToDiscarded(any()); // ignoring cancellation event,
cancelJob(jobId); cancelJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
@ -256,7 +256,7 @@ public class JobManagerTest extends AbstractControllerTest {
Thread.sleep(3000); Thread.sleep(3000);
verify(jobStatsService, never()).reportTaskResult(any(), any(), any()); 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 @Test
@ -340,7 +340,7 @@ public class JobManagerTest extends AbstractControllerTest {
} }
@Test @Test
public void testGeneralJobError() { public void testSubmitJob_generalError() {
int submittedTasks = 100; int submittedTasks = 100;
JobId jobId = jobManager.submitJob(Job.builder() JobId jobId = jobManager.submitJob(Job.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -358,7 +358,7 @@ public class JobManagerTest extends AbstractControllerTest {
Job job = findJobById(jobId); Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED); assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, submittedTasks); assertThat(job.getResult().getSuccessfulCount()).isBetween(1, submittedTasks);
assertThat(job.getResult().getDiscardedCount()).isBetween(1, submittedTasks); assertThat(job.getResult().getDiscardedCount()).isZero();
assertThat(job.getResult().getTotalCount()).isNull(); assertThat(job.getResult().getTotalCount()).isNull();
}); });
@ -369,7 +369,70 @@ public class JobManagerTest extends AbstractControllerTest {
} }
@Test @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 successfulTasks = 3;
int failedTasks = 2; int failedTasks = 2;
int totalTasksCount = successfulTasks + failedTasks; int totalTasksCount = successfulTasks + failedTasks;
@ -410,11 +473,12 @@ public class JobManagerTest extends AbstractControllerTest {
assertThat(job.getResult().getFailedCount()).isZero(); assertThat(job.getResult().getFailedCount()).isZero();
assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount); assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount);
assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getResult().getResults()).isEmpty();
assertThat(job.getConfiguration().getToReprocess()).isNullOrEmpty();
}); });
} }
@Test @Test
public void testJobReprocessing_somePermanentlyFailed() throws Exception { public void testReprocessJob_somePermanentlyFailed() throws Exception {
int successfulTasks = 3; int successfulTasks = 3;
int failedTasks = 2; int failedTasks = 2;
int permanentlyFailedTasks = 1; int permanentlyFailedTasks = 1;

View File

@ -36,7 +36,7 @@ public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest {
} }
@Override @Override
public void testGeneralJobError() { public void testSubmitJob_generalError() {
} }

View File

@ -87,6 +87,8 @@ public interface TbClusterService extends TbQueueClusterService {
void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
void broadcast(ComponentLifecycleMsg componentLifecycleMsg);
void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback); void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback); void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback);

View File

@ -40,4 +40,6 @@ public interface JobService extends EntityDaoService {
Job findLatestJobByKey(TenantId tenantId, String key); Job findLatestJobByKey(TenantId tenantId, String key);
void deleteJob(TenantId tenantId, JobId jobId);
} }

View File

@ -20,6 +20,7 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List; import java.util.List;
@ -28,6 +29,7 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
@ToString(callSuper = true)
public class DummyJobConfiguration extends JobConfiguration { public class DummyJobConfiguration extends JobConfiguration {
private long taskProcessingTimeMs; private long taskProcessingTimeMs;

View File

@ -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.JobId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@ToString(callSuper = true) @ToString(callSuper = true)
@ -42,19 +44,26 @@ public class Job extends BaseData<JobId> implements HasTenantId {
private String key; private String key;
@NotBlank @NotBlank
private String description; private String description;
@NotNull
private JobStatus status; private JobStatus status;
@NotNull @NotNull
@Valid @Valid
private JobConfiguration configuration; private JobConfiguration configuration;
@NotNull
private JobResult result; private JobResult result;
@Builder @Builder(toBuilder = true)
public Job(TenantId tenantId, JobType type, String key, String description, JobConfiguration configuration) { public Job(TenantId tenantId, JobType type, String key, String description, JobConfiguration configuration) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.type = type; this.type = type;
this.key = key; this.key = key;
this.description = description; this.description = description;
this.configuration = configuration; this.configuration = configuration;
this.configuration.setTasksKey(UUID.randomUUID().toString());
presetResult();
}
public void presetResult() {
this.result = switch (type) { this.result = switch (type) {
case DUMMY -> new DummyJobResult(); case DUMMY -> new DummyJobResult();
}; };

View File

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.job.task.TaskResult;
@ -34,7 +35,9 @@ import java.util.List;
@Data @Data
public abstract class JobConfiguration implements Serializable { public abstract class JobConfiguration implements Serializable {
private List<TaskResult> toReprocess; @NotBlank
private String tasksKey; // internal
private List<TaskResult> toReprocess; // internal
@JsonIgnore @JsonIgnore
public abstract JobType getType(); public abstract JobType getType();

View File

@ -46,7 +46,7 @@ public class DummyTask extends Task<DummyTaskResult> {
@Override @Override
public DummyTaskResult toDiscarded() { public DummyTaskResult toDiscarded() {
return DummyTaskResult.discarded(); return DummyTaskResult.discarded(this);
} }
@Override @Override

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.job.task;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.JobType;
@ -25,29 +26,34 @@ import org.thingsboard.server.common.data.job.JobType;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder @SuperBuilder
@ToString(callSuper = true)
public class DummyTaskResult extends TaskResult { 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; private DummyTaskFailure failure;
public static DummyTaskResult success() { public static DummyTaskResult success(DummyTask task) {
return SUCCESS; return DummyTaskResult.builder()
.key(task.getKey())
.success(true)
.build();
} }
public static DummyTaskResult failed(DummyTask task, Throwable error) { public static DummyTaskResult failed(DummyTask task, Throwable error) {
DummyTaskResult result = new DummyTaskResult(); return DummyTaskResult.builder()
result.setFailure(DummyTaskFailure.builder() .key(task.getKey())
.failure(DummyTaskFailure.builder()
.error(error.getMessage()) .error(error.getMessage())
.number(task.getNumber()) .number(task.getNumber())
.failAlways(task.isFailAlways()) .failAlways(task.isFailAlways())
.build()); .build())
return result; .build();
} }
public static DummyTaskResult discarded() { public static DummyTaskResult discarded(DummyTask task) {
return DISCARDED; return DummyTaskResult.builder()
.key(task.getKey())
.discarded(true)
.build();
} }
@Override @Override

View File

@ -40,6 +40,7 @@ public abstract class Task<R extends TaskResult> {
private TenantId tenantId; private TenantId tenantId;
private JobId jobId; private JobId jobId;
private String key;
private int retries; private int retries;
public Task() { public Task() {

View File

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.job.JobType;
}) })
public abstract class TaskResult { public abstract class TaskResult {
private String key;
private boolean success; private boolean success;
private boolean discarded; private boolean discarded;

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.msg.plugin; package org.thingsboard.server.common.msg.plugin;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -45,13 +46,14 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
private final String name; private final String name;
private final EntityId oldProfileId; private final EntityId oldProfileId;
private final EntityId profileId; private final EntityId profileId;
private final JsonNode info;
public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) { 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 @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.tenantId = tenantId;
this.entityId = entityId; this.entityId = entityId;
this.event = event; this.event = event;
@ -59,6 +61,7 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
this.name = name; this.name = name;
this.oldProfileId = oldProfileId; this.oldProfileId = oldProfileId;
this.profileId = profileId; this.profileId = profileId;
this.info = info;
} }
public Optional<RuleChainId> getRuleChainId() { public Optional<RuleChainId> getRuleChainId() {

View File

@ -139,6 +139,9 @@ public class ProtoUtils {
if (msg.getOldName() != null) { if (msg.getOldName() != null) {
builder.setOldName(msg.getOldName()); builder.setOldName(msg.getOldName());
} }
if (msg.getInfo() != null) {
builder.setInfo(JacksonUtil.toString(msg.getInfo()));
}
return builder.build(); return builder.build();
} }
@ -166,6 +169,9 @@ public class ProtoUtils {
var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE;
builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB()))); builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB())));
} }
if (proto.hasInfo()) {
builder.info(JacksonUtil.toJsonNode(proto.getInfo()));
}
return builder.build(); return builder.build();
} }

View File

@ -1261,6 +1261,7 @@ message ComponentLifecycleMsgProto {
int64 oldProfileIdLSB = 10; int64 oldProfileIdLSB = 10;
int64 profileIdMSB = 11; int64 profileIdMSB = 11;
int64 profileIdLSB = 12; int64 profileIdLSB = 12;
optional string info = 13;
} }
message EdgeEventMsgProto { message EdgeEventMsgProto {

View File

@ -68,7 +68,9 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
private MainQueueConsumerManager<TbProtoQueueMsg<TaskProto>, QueueConfig> taskConsumer; private MainQueueConsumerManager<TbProtoQueueMsg<TaskProto>, QueueConfig> taskConsumer;
private final ExecutorService taskExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor")); private final ExecutorService taskExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getJobType().name().toLowerCase() + "-task-processor"));
private final SetCache<UUID> discardedJobs = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); private final SetCache<String> discarded = new SetCache<>(TimeUnit.MINUTES.toMillis(60));
private final SetCache<String> failed = new SetCache<>(TimeUnit.MINUTES.toMillis(60));
private final SetCache<UUID> deletedTenants = new SetCache<>(TimeUnit.MINUTES.toMillis(60)); private final SetCache<UUID> deletedTenants = new SetCache<>(TimeUnit.MINUTES.toMillis(60));
@PostConstruct @PostConstruct
@ -98,9 +100,13 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
EntityId entityId = event.getEntityId(); EntityId entityId = event.getEntityId();
switch (entityId.getEntityType()) { switch (entityId.getEntityType()) {
case JOB -> { case JOB -> {
String tasksKey = event.getInfo().get("tasksKey").asText();
if (event.getEvent() == ComponentLifecycleEvent.STOPPED) { if (event.getEvent() == ComponentLifecycleEvent.STOPPED) {
log.info("Adding job {} to discarded", entityId); log.info("Adding job {} ({}) to discarded", entityId, tasksKey);
addToDiscardedJobs(entityId.getId()); addToDiscarded(tasksKey);
} else if (event.getEvent() == ComponentLifecycleEvent.FAILED) {
log.info("Adding job {} ({}) to failed", entityId, tasksKey);
failed.add(tasksKey);
} }
} }
case TENANT -> { case TENANT -> {
@ -117,14 +123,18 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
try { try {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
T task = (T) JacksonUtil.fromString(msg.getValue().getValue(), Task.class); T task = (T) JacksonUtil.fromString(msg.getValue().getValue(), Task.class);
if (discardedJobs.contains(task.getJobId().getId())) { if (discarded.contains(task.getKey())) {
log.debug("Skipping task for cancelled job {}: {}", task.getJobId(), task); log.debug("Skipping task for discarded job {}: {}", task.getJobId(), task);
reportTaskDiscarded(task); reportTaskDiscarded(task);
continue; 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())) { } else if (deletedTenants.contains(task.getTenantId().getId())) {
log.debug("Skipping task for deleted tenant {}: {}", task.getTenantId(), task); log.debug("Skipping task for deleted tenant {}: {}", task.getTenantId(), task);
continue; continue;
} }
processTask(task); processTask(task);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw e; throw e;
@ -185,8 +195,8 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result);
} }
public void addToDiscardedJobs(UUID jobId) { public void addToDiscarded(String tasksKey) {
discardedJobs.add(jobId); discarded.add(tasksKey);
} }
protected <V> V wait(Future<V> future) throws Exception { protected <V> V wait(Future<V> future) throws Exception {

View File

@ -37,9 +37,10 @@ import com.google.common.collect.Lists;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.DataType;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.Views;
import java.io.File; import java.io.File;
import java.io.IOException; 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> T fromString(String string, Class<T> clazz) { public static <T> T fromString(String string, Class<T> clazz) {
try { try {
return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null;

View File

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.ConstraintValidator;
import java.util.Optional; import java.util.Optional;
@ -119,6 +120,11 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
boolean publishEvent = false; boolean publishEvent = false;
for (TaskResult taskResult : jobStats.getTaskResults()) { 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); result.processTaskResult(taskResult);
if (result.getCancellationTs() > 0) { if (result.getCancellationTs() > 0) {
@ -142,6 +148,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
publishEvent = true; publishEvent = true;
} }
result.setFinishTs(System.currentTimeMillis()); 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) { private Job saveJob(TenantId tenantId, Job job, boolean publishEvent, JobStatus prevStatus) {
ConstraintValidator.validateFields(job);
job = jobDao.save(tenantId, job); job = jobDao.save(tenantId, job);
if (publishEvent) { if (publishEvent) {
eventPublisher.publishEvent(SaveEntityEvent.builder() eventPublisher.publishEvent(SaveEntityEvent.builder()
@ -186,6 +194,15 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
return jobDao.findLatestByTenantIdAndKey(tenantId, key); 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) { private Job findForUpdate(TenantId tenantId, JobId jobId) {
return jobDao.findByIdForUpdate(tenantId, jobId); return jobDao.findByIdForUpdate(tenantId, jobId);
} }