diff --git a/core/pom.xml b/core/pom.xml index b827fc4d9..e259beabe 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -156,6 +156,10 @@ ../ui/spt-ui-lib/lib/server-api.ts module implementationFile + + jakarta.annotation.Nullable + org.springframework.lang.Nullable + diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/PersistentTaskService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/PersistentTaskService.java index b83411bfe..90cec913c 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/PersistentTaskService.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/PersistentTaskService.java @@ -10,7 +10,6 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.context.event.EventListener; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Direction; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -21,7 +20,9 @@ import org.sterl.spring.persistent_tasks.api.event.TriggerTaskCommand; import org.sterl.spring.persistent_tasks.history.HistoryService; import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; import org.sterl.spring.persistent_tasks.scheduler.SchedulerService; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import org.sterl.spring.persistent_tasks.trigger.TriggerService; import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; @@ -47,27 +48,19 @@ public class PersistentTaskService { * @param key the {@link TriggerKey} to look for * @return the {@link TriggerEntity} to the {@link TriggerKey} */ - public Optional getLastTriggerData(TriggerKey key) { + public Optional getLastTriggerData(TriggerKey key) { final Optional trigger = triggerService.get(key); if (trigger.isEmpty()) { var history = historyService.findLastKnownStatus(key); if (history.isPresent()) { - return Optional.ofNullable(history.get().getData()); + return Optional.ofNullable(history.get()); } return Optional.empty(); } else { - return Optional.ofNullable(trigger.get().getData()); + return Optional.ofNullable(trigger.get()); } } - public Optional getLastDetailData(TriggerKey key) { - var data = historyService.findAllDetailsForKey(key, Pageable.ofSize(1)); - if (data.isEmpty()) { - return Optional.empty(); - } - return Optional.of(data.getContent().get(0).getData()); - } - @EventListener void queue(TriggerTaskCommand event) { if (event.size() == 1) { @@ -167,4 +160,10 @@ public Optional findLastTriggerByCorrelationId(String correlation } return result.isEmpty() ? Optional.empty() : Optional.of(result.getFirst()); } + + public Optional getLastTriggerHistory(Long id) { + var result = historyService.findAllDetailsForInstance(id, PageRequest.ofSize(1)); + if (result.isEmpty()) return Optional.empty(); + return Optional.of(result.getContent().getFirst()); + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryTrigger.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryTrigger.java new file mode 100644 index 000000000..8ba71b605 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryTrigger.java @@ -0,0 +1,31 @@ +package org.sterl.spring.persistent_tasks.api; + +import java.time.OffsetDateTime; + +import org.springframework.lang.Nullable; + +import lombok.Data; + +@Data +public class HistoryTrigger { + + /** just a unique id of this trigger */ + private Long id; + + private Long instanceId; + + /** the business key which is unique it is combination for triggers but not the history! */ + private TriggerKey key; + + private OffsetDateTime createdTime; + + @Nullable + private OffsetDateTime start; + + private int executionCount = 0; + + private TriggerStatus status; + + @Nullable + private String message; +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java index 719ab4274..cefa3ac66 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java @@ -2,6 +2,8 @@ import java.time.OffsetDateTime; +import org.springframework.lang.Nullable; + import lombok.Data; @Data @@ -15,20 +17,26 @@ public class Trigger { /** the business key which is unique it is combination for triggers but not the history! */ private TriggerKey key; + @Nullable private String tag; + @Nullable private String correlationId; + @Nullable private String runningOn; - private OffsetDateTime createdTime = OffsetDateTime.now(); + private OffsetDateTime createdTime; - private OffsetDateTime runAt = OffsetDateTime.now(); + private OffsetDateTime runAt; + @Nullable private OffsetDateTime lastPing; + @Nullable private OffsetDateTime start; + @Nullable private OffsetDateTime end; private int executionCount = 0; @@ -38,10 +46,14 @@ public class Trigger { private TriggerStatus status = TriggerStatus.WAITING; + @Nullable private Long runningDurationInMs; + @Nullable private Object state; + @Nullable private String exceptionName; + @Nullable private String lastException; } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerSearch.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerSearch.java index 229f978e6..e06db2f21 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerSearch.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerSearch.java @@ -26,17 +26,18 @@ public boolean hasValue() { || StringHelper.hasValue(tag); } - public static TriggerSearch byCorrelationId(String correlationId) { var result = new TriggerSearch(); result.setCorrelationId(correlationId); return result; } + public static TriggerSearch byStatus(TriggerStatus status) { var result = new TriggerSearch(); result.setStatus(status); return result; } + public static TriggerSearch forTriggerRequest(TriggerRequest trigger) { var search = new TriggerSearch(); if (trigger.key() != null) { @@ -57,6 +58,7 @@ public static TriggerSearch forTriggerRequest(TriggerRequest trigger) { public static Sort sortByCreatedTime(Direction direction) { return Sort.by(direction, "data.createdTime"); } + public static Pageable applyDefaultSortIfNeeded(Pageable page) { var result = page; if (page.getSort() == Sort.unsorted()) { @@ -64,5 +66,4 @@ public static Pageable applyDefaultSortIfNeeded(Pageable page) { } return result; } - } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryService.java index 0158b7a6d..6a4602f14 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryService.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryService.java @@ -22,6 +22,7 @@ import org.sterl.spring.persistent_tasks.history.model.QCompletedTriggerEntity; import org.sterl.spring.persistent_tasks.history.repository.CompletedTriggerRepository; import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository; +import org.sterl.spring.persistent_tasks.shared.QueryHelper; import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService; @@ -53,9 +54,10 @@ public void deleteAll() { triggerHistoryDetailRepository.deleteAllInBatch(); } - public void deleteAllOlderThan(OffsetDateTime age) { - completedTriggerRepository.deleteOlderThan(age); - triggerHistoryDetailRepository.deleteOlderThan(age); + public long deleteAllOlderThan(OffsetDateTime age) { + var result = triggerHistoryDetailRepository.deleteOlderThan(age); + result += completedTriggerRepository.deleteOlderThan(age); + return result; } /** @@ -65,16 +67,9 @@ public long countTriggers(TriggerStatus status) { return triggerHistoryDetailRepository.countByStatus(status); } - public List findAllDetailsForInstance(long instanceId) { - return triggerHistoryDetailRepository.findAllByInstanceId(instanceId); - } - - public Page findAllDetailsForKey(TriggerKey key) { - return findAllDetailsForKey(key, PageRequest.of(0, 100)); - } - public Page findAllDetailsForKey(TriggerKey key, Pageable page) { - page = applyDefaultSortIfNeeded(page); - return triggerHistoryDetailRepository.listKnownStatusFor(key, page); + public Page findAllDetailsForInstance(long instanceId, Pageable page) { + page = QueryHelper.applySortIfEmpty(page, Sort.by(Direction.DESC, "id")); + return triggerHistoryDetailRepository.findAllByInstanceId(instanceId, page); } public Optional reQueue(Long id, OffsetDateTime runAt) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryTimer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryTimer.java index abcc6ee79..da0b39fcd 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryTimer.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryTimer.java @@ -30,8 +30,8 @@ class HistoryTimer { void deleteOldHistory() { try { final var age = OffsetDateTime.now().minus(historyTimeout); - historyService.deleteAllOlderThan(age); - log.debug("Deleted triggers older than {}.", historyTimeout); + var count = historyService.deleteAllOlderThan(age); + log.debug("Deleted history {} older than {}.", count, historyTimeout); } catch (Exception e) { log.error("Failed to delete old triggers", e); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java index da36fc5d3..851d60384 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java @@ -1,7 +1,9 @@ package org.sterl.spring.persistent_tasks.history.api; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.sterl.spring.persistent_tasks.api.Trigger; +import org.sterl.spring.persistent_tasks.api.HistoryTrigger; import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; @@ -22,16 +24,23 @@ public Trigger convert(@NonNull CompletedTriggerEntity source) { } } - enum FromTriggerStateDetailEntity implements ExtendetConvert { + enum ToHistoryTrigger implements ExtendetConvert { INSTANCE; - @NonNull @Override - public Trigger convert(@NonNull HistoryTriggerEntity source) { - var result = ToTrigger.INSTANCE.convert(source); + @Nullable + public HistoryTrigger convert(@NonNull HistoryTriggerEntity source) { + var result = new HistoryTrigger(); + result.setCreatedTime(source.getCreatedTime()); + result.setExecutionCount(source.getExecutionCount()); result.setId(source.getId()); result.setInstanceId(source.getInstanceId()); + result.setKey(source.getKey()); + result.setMessage(source.getMessage()); + result.setStart(source.getStart()); + result.setStatus(source.getStatus()); return result; } + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java index c93f87ad0..13983e14f 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java @@ -15,11 +15,12 @@ import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.TriggerGroup; +import org.sterl.spring.persistent_tasks.api.HistoryTrigger; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.history.HistoryService; import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.FromLastTriggerStateEntity; -import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.FromTriggerStateDetailEntity; +import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.ToHistoryTrigger; import lombok.RequiredArgsConstructor; @@ -32,9 +33,11 @@ public class TriggerHistoryResource { private final HistoryService historyService; @GetMapping("history/instance/{instanceId}") - public List listInstances(@PathVariable("instanceId") long instanceId) { - return FromTriggerStateDetailEntity.INSTANCE.convert( // - historyService.findAllDetailsForInstance(instanceId)); + public PagedModel listInstances( + @PathVariable("instanceId") long instanceId, + @PageableDefault(size = 250) Pageable page) { + + return ToHistoryTrigger.INSTANCE.toPage(historyService.findAllDetailsForInstance(instanceId, page)); } @GetMapping("task-status-history") public List taskStatusHistory() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/component/TriggerHistoryComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/component/TriggerHistoryComponent.java index 4cd8703ed..98c96b294 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/component/TriggerHistoryComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/component/TriggerHistoryComponent.java @@ -1,7 +1,6 @@ package org.sterl.spring.persistent_tasks.history.component; -import java.time.OffsetDateTime; - +import org.apache.commons.lang3.StringUtils; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @@ -64,11 +63,16 @@ public void execute(final long triggerId, final TriggerEntity data, boolean isDo } var detail = new HistoryTriggerEntity(); + detail.setExecutionCount(data.getExecutionCount()); detail.setInstanceId(triggerId); - detail.setData(data.toBuilder() - .state(null) - .createdTime(OffsetDateTime.now()) - .build()); + detail.setKey(data.getKey()); + + var msg = data.getExceptionName(); + if (data.getLastException() != null) msg = data.getLastException(); + detail.setMessage(StringUtils.substring(msg, 0, 200)); + + detail.setStart(data.getStart()); + detail.setStatus(data.getStatus()); triggerHistoryDetailRepository.save(detail); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java index 0819fc916..721cd8b26 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java @@ -21,12 +21,12 @@ @Entity @Table(name = "pt_completed_triggers", indexes = { - @Index(name = "idx_pt_trigger_history_last_states_task_name", columnList = "task_name"), - @Index(name = "idx_pt_trigger_history_last_states_trigger_id", columnList = "trigger_id"), - @Index(name = "idx_pt_trigger_history_last_states_status", columnList = "status"), - @Index(name = "idx_pt_trigger_history_last_states_created_time", columnList = "created_time"), - @Index(name = "idx_pt_trigger_history_last_states_correlation_id", columnList = "correlation_id"), - @Index(name = "idx_pt_trigger_history_last_states_tag", columnList = "tag"), + @Index(name = "idx_pt_completed_triggers_task_name", columnList = "task_name"), + @Index(name = "idx_pt_completed_triggers_trigger_id", columnList = "trigger_id"), + @Index(name = "idx_pt_completed_triggers_status", columnList = "status"), + @Index(name = "idx_pt_completed_triggers_created_time", columnList = "created_time"), + @Index(name = "idx_pt_completed_triggers_correlation_id", columnList = "correlation_id"), + @Index(name = "idx_pt_completed_triggers_tag", columnList = "tag"), }) @Data @NoArgsConstructor diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java index 9e226a129..1dd853e8e 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java @@ -3,12 +3,16 @@ import java.time.OffsetDateTime; import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,6 +21,7 @@ import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Builder.Default; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -26,41 +31,51 @@ */ @Entity @Table(name = "pt_trigger_history", indexes = { - @Index(name = "idx_pt_trigger_history_instance_id", columnList = "instance_id"), - @Index(name = "idx_pt_trigger_history_name", columnList = "task_name"), - @Index(name = "idx_pt_trigger_history_trigger_id", columnList = "trigger_id"), - @Index(name = "idx_pt_trigger_history_status", columnList = "status"), - @Index(name = "idx_pt_trigger_history_created_time", columnList = "created_time"), - @Index(name = "idx_pt_trigger_history_correlation_id", columnList = "correlation_id"), - @Index(name = "idx_pt_trigger_history_tag", columnList = "tag"), + @Index(name = "idx_pt_trigger_history_instance_id", columnList = "instance_id"), }) @Data @NoArgsConstructor @Builder(toBuilder = true) @AllArgsConstructor @EqualsAndHashCode(of = "id") -public class HistoryTriggerEntity implements HasTrigger { +public class HistoryTriggerEntity { - @GeneratedValue(generator = "seq_pt_trigger_history_details", strategy = GenerationType.SEQUENCE) + @GeneratedValue(generator = "seq_pt_trigger_history", strategy = GenerationType.SEQUENCE) @Column(updatable = false) @Id private Long id; + + @Enumerated(EnumType.STRING) + @Column(updatable = false, nullable = false) + private TriggerStatus status; /** * The original ID of this trigger in case grouping is needed * as for each trigger multiple history entries are added. */ + @Column(name = "instance_id", updatable = false) private Long instanceId; @Embedded + @AttributeOverrides(@AttributeOverride( + name = "id", + column = @Column(name = "trigger_id", nullable = false, length = 200, updatable = false) + ) + ) + private TriggerKey key; + + @Default @NotNull - private TriggerEntity data; + @Column(nullable = false, updatable = false, name = "created_time") + private OffsetDateTime createdTime = OffsetDateTime.now(); - public TriggerKey getKey() { - return data.getKey(); - } + @Column(name = "start_time", updatable = false) + private OffsetDateTime start; + + @Column(updatable = false) + private int executionCount; + + @Column(length = 200, updatable = false) + private String message; - public void setCreatedTime(OffsetDateTime time) { - this.data.setCreatedTime(time); - } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java index 46a9e96da..01ce4e2f2 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java @@ -2,11 +2,16 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; +import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.shared.repository.TriggerRepository; -public interface CompletedTriggerRepository extends HistoryTriggerRepository { +public interface CompletedTriggerRepository extends TriggerRepository { @Query(""" SELECT new org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview( @@ -25,4 +30,10 @@ public interface CompletedTriggerRepository extends HistoryTriggerRepository listTriggerStatus(); + + @Query(""" + SELECT e FROM #{#entityName} e + WHERE e.data.key = :key + """) + Page listKnownStatusFor(@Param("key") TriggerKey key, Pageable page); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/HistoryTriggerRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/HistoryTriggerRepository.java deleted file mode 100644 index 37d62d881..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/HistoryTriggerRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sterl.spring.persistent_tasks.history.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.NoRepositoryBean; -import org.springframework.data.repository.query.Param; -import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; -import org.sterl.spring.persistent_tasks.shared.repository.TriggerRepository; - -@NoRepositoryBean -public interface HistoryTriggerRepository extends TriggerRepository { - - @Query(""" - SELECT e FROM #{#entityName} e - WHERE e.data.key = :key - """) - Page listKnownStatusFor(@Param("key") TriggerKey key, Pageable page); -} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepository.java index 2ba576f1d..160618194 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepository.java @@ -1,18 +1,39 @@ package org.sterl.spring.persistent_tasks.history.repository; -import java.util.List; +import java.time.OffsetDateTime; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; +import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; -public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository { +public interface TriggerHistoryDetailRepository + extends JpaRepository, QuerydslPredicateExecutor { - @Query(""" - SELECT e - FROM #{#entityName} e - WHERE e.instanceId = :instanceId - ORDER BY e.id DESC - """) - List findAllByInstanceId(@Param("instanceId") long instanceId); + @Query(""" + SELECT e + FROM #{#entityName} e + WHERE e.instanceId = :instanceId + """) + Page findAllByInstanceId( + @Param("instanceId") long instanceId, Pageable page); + + @Query(""" + DELETE FROM #{#entityName} e + WHERE e.createdTime < :age + """) + @Modifying + long deleteOlderThan(@Param("age") OffsetDateTime age); + + @Query(""" + SELECT COUNT(e.id) + FROM #{#entityName} e + WHERE e.status = :status + """) + long countByStatus(@Param("status") TriggerStatus status); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java index c6bee8b61..104c5171b 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java @@ -43,8 +43,8 @@ void rescheduleAbandonedTasks() { for (SchedulerService s : schedulerServices) { try { final var count = s.rescheduleAbandonedTriggers(timeout); - log.debug("Found {} abandoned tasks for {}. Timeout={}", - count.size(), s.getName(), timeout); + log.info("Found {} abandoned tasks for {}. Timeout={}", + count.size(), s.getName(), taskTimeout); } catch (Exception e) { log.error("Scheduler {} failed schedule abandoned tasks", s.getName(), e); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/DateUtil.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/DateUtil.java new file mode 100644 index 000000000..370323775 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/DateUtil.java @@ -0,0 +1,11 @@ +package org.sterl.spring.persistent_tasks.shared; + +import java.time.OffsetDateTime; + +public class DateUtil { + + public static long secondsBeetween(OffsetDateTime start, long secondsEnd) { + if (start == null) return 0; + return secondsEnd - start.toEpochSecond(); + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/QueryHelper.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/QueryHelper.java index d69adb7dd..98cd156c4 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/QueryHelper.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/QueryHelper.java @@ -1,5 +1,8 @@ package org.sterl.spring.persistent_tasks.shared; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -8,6 +11,14 @@ import com.querydsl.core.types.dsl.StringPath; public class QueryHelper { + + public static Pageable applySortIfEmpty(Pageable page, Sort sort) { + Pageable result = page; + if (page.getSort() == null || page.getSort() == Sort.unsorted()) { + result = PageRequest.of(page.getPageNumber(), page.getPageSize(), sort); + } + return result; + } @Nullable public static Predicate eq(@NonNull SimpleExpression path, @Nullable T value) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java index 409fcf3e7..fa2a5775b 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java @@ -4,6 +4,7 @@ import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer; public enum ToTrigger implements ExtendetConvert { @@ -14,7 +15,7 @@ public enum ToTrigger implements ExtendetConvert { @NonNull @Override public Trigger convert(@NonNull HasTrigger hasData) { - final var source = hasData.getData(); + final TriggerEntity source = hasData.getData(); final var result = new Trigger(); result.setKey(source.getKey()); result.setCorrelationId(source.getCorrelationId()); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java index ac9dfe600..3397bd977 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java @@ -7,6 +7,8 @@ import org.sterl.spring.persistent_tasks.api.TriggerStatus; public interface HasTrigger { + Long getId(); + TriggerEntity getData(); default TriggerKey key() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerRepository.java index 14a89ef0f..7f82c9297 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerRepository.java @@ -103,13 +103,13 @@ default Page findByGroup( Page findAll(@Param("taskName") String taskName, Pageable page); @Query(""" - SELECT COUNT(e.data.key) + SELECT COUNT(e.id) FROM #{#entityName} e WHERE e.data.key.taskName = :taskName """) long countByTaskName(@Param("taskName") String taskName); @Query(""" - SELECT COUNT(e.data.key) + SELECT COUNT(e.id) FROM #{#entityName} e WHERE e.data.key = :key """) long countByKey(@Param("key") TriggerKey key); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java index a15f26a0c..9f9163a68 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java @@ -18,6 +18,7 @@ import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.api.TriggerStatus; +import org.sterl.spring.persistent_tasks.shared.DateUtil; import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService; import org.sterl.spring.persistent_tasks.task.TaskService; import org.sterl.spring.persistent_tasks.trigger.component.EditTriggerComponent; @@ -206,10 +207,15 @@ public long countTriggers(@Nullable TriggerStatus status) { public List rescheduleAbandoned(OffsetDateTime timeout) { final List result = readTrigger.findTriggersLastPingAfter( timeout); - final var e = new IllegalStateException("Trigger abandoned - timeout: " + timeout); + var now = OffsetDateTime.now().toEpochSecond(); result.forEach(t -> { - var task = taskService.get(t.newTaskId()); - var state = stateSerializer.deserializeOrNull(t.getData().getState()); + final var task = taskService.get(t.newTaskId()); + final var state = stateSerializer.deserializeOrNull(t.getData().getState()); + + final var e = new IllegalStateException("Trigger abandoned. Timeout: " + + timeout + " running on: " + t.getRunningOn() + + " since: " + DateUtil.secondsBeetween(t.getData().getStart(), now)); + failTrigger.execute(task.orElse(null), t, state, e); }); log.debug("rescheduled {} triggers", result.size()); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponent.java index 190bbc042..bd5f3dbb3 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponent.java @@ -56,9 +56,16 @@ public boolean hasPendingTriggers() { } return false; } - + + /** + * Searches for all triggers which are still running but the ping is long ago. + * @param dateTime the ping should be before this date + * @return all found triggers - never null + */ public List findTriggersLastPingAfter(OffsetDateTime dateTime) { - return triggerRepository.findTriggersLastPingAfter(dateTime); + return triggerRepository.findTriggersLastPingAfter( + TriggerStatus.RUNNING, + dateTime); } public Page searchTriggers(@Nullable TriggerSearch search, Pageable page) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java index 068918a9e..59745e778 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java @@ -1,5 +1,6 @@ package org.sterl.spring.persistent_tasks.trigger.component; +import java.time.OffsetDateTime; import java.util.Optional; import org.springframework.lang.Nullable; @@ -25,7 +26,8 @@ public class RunTriggerComponent { private final StateSerializer serializer = new StateSerializer(); /** - * Will execute the given {@link RunningTriggerEntity} and handle any errors etc. + * Will execute the given {@link RunningTriggerEntity} and handle any errors + * etc. */ @Transactional(propagation = Propagation.NEVER) public Optional execute(RunningTriggerEntity trigger) { @@ -35,9 +37,17 @@ public Optional execute(RunningTriggerEntity trigger) { final var runTaskWithState = buildTaskWithStateFor(trigger); // something went really wrong this trigger is crap - if (runTaskWithState == null) return Optional.of(trigger); + if (runTaskWithState == null) + return Optional.of(trigger); try { + if (OffsetDateTime.now().isAfter(trigger.getData().getRunAt())) { + log.debug("Running {} for {} time.", trigger.key(), trigger.executionCount()); + } else { + log.info("Running to early {} start should be {} for {} time.", + trigger.key(), trigger.getData().getRunAt(), trigger.executionCount()); + } + RunningTriggerContextHolder.setContext(runTaskWithState.runningTrigger()); return runTaskWithState.execute(editTrigger); } catch (Exception e) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerAddedEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerAddedEvent.java index 057ad3163..c18b6f4e3 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerAddedEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerAddedEvent.java @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerAddedEvent(long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { +public record TriggerAddedEvent(Long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { @Override public boolean isDone() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerCanceledEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerCanceledEvent.java index 79e24d506..dd14a6c20 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerCanceledEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerCanceledEvent.java @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerCanceledEvent(long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { +public record TriggerCanceledEvent(Long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { @Override public boolean isDone() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerExpiredEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerExpiredEvent.java index 431363ab3..752a49d2b 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerExpiredEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerExpiredEvent.java @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerExpiredEvent(long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { +public record TriggerExpiredEvent(Long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { @Override public boolean isDone() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerFailedEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerFailedEvent.java index dc72339b0..93c0d6028 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerFailedEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerFailedEvent.java @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerFailedEvent(long id, +public record TriggerFailedEvent(Long id, TriggerEntity data, Serializable state, Exception exception, OffsetDateTime retryAt) implements TriggerLifeCycleEvent { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerLifeCycleEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerLifeCycleEvent.java index 4b5cdc1e3..a86a37afa 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerLifeCycleEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerLifeCycleEvent.java @@ -1,6 +1,8 @@ package org.sterl.spring.persistent_tasks.trigger.event; import java.io.Serializable; +import java.time.Duration; +import java.time.OffsetDateTime; import org.springframework.lang.Nullable; import org.sterl.spring.persistent_tasks.api.event.PersistentTasksEvent; @@ -15,12 +17,30 @@ public interface TriggerLifeCycleEvent extends HasTrigger, PersistentTasksEvent default TriggerEntity getData() { return data(); } - long id(); + + Long id(); + + default Long getId() { + return id(); + } + TriggerEntity data(); + @Nullable Serializable state(); + /** + * If a trigger is done, it finished it's execution either successfully or with an final error without retries left. + * * @return true if the trigger was completed, either with success, error or canceled. */ boolean isDone(); + + default long timePassedMs() { + var result = data().getRunningDurationInMs(); + if (result == null && data().getStart() != null) { + result = Duration.between(data().getStart(), OffsetDateTime.now()).toMillis(); + } + return result == null ? 0 : result.longValue(); + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerResumedEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerResumedEvent.java index 032f5d3b2..0520b99ba 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerResumedEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerResumedEvent.java @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerResumedEvent(long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { +public record TriggerResumedEvent(Long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { @Override public boolean isDone() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java index e8f2924be..701023e80 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java @@ -10,7 +10,7 @@ * This event is maybe not in a transaction and so a transactional event listener will not work. *

*/ -public record TriggerRunningEvent(long id, TriggerEntity data, Serializable state, String runningOn) implements TriggerLifeCycleEvent { +public record TriggerRunningEvent(Long id, TriggerEntity data, Serializable state, String runningOn) implements TriggerLifeCycleEvent { public boolean isRunningOn(String name) { return isRunning() && name != null && name.equals(runningOn); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerSuccessEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerSuccessEvent.java index 45690b68d..e13e3f784 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerSuccessEvent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerSuccessEvent.java @@ -9,7 +9,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerSuccessEvent(long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { +public record TriggerSuccessEvent(Long id, TriggerEntity data, Serializable state) implements TriggerLifeCycleEvent { @Override public boolean isDone() { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/TriggerMetricInterceptor.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/TriggerMetricInterceptor.java index e47e74b06..5ec496fcc 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/TriggerMetricInterceptor.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/TriggerMetricInterceptor.java @@ -7,9 +7,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.trigger.event.TriggerCanceledEvent; -import org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent; -import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerLifeCycleEvent; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; @@ -26,23 +24,12 @@ public class TriggerMetricInterceptor { private final Map cache = new ConcurrentHashMap<>(); @EventListener - public void onFailed(TriggerFailedEvent data) { - recordTime(data.key().getTaskName(), - data.status(), - data.getData().getRunningDurationInMs()); - } - @EventListener - public void onSuccess(TriggerSuccessEvent data) { - recordTime(data.key().getTaskName(), - data.status(), - data.getData().getRunningDurationInMs()); - - } - @EventListener - public void onSuccess(TriggerCanceledEvent data) { - recordTime(data.key().getTaskName(), - data.status(), - data.getData().getRunningDurationInMs()); + public void onFailed(TriggerLifeCycleEvent data) { + if (data.isDone()) { + recordTime(data.key().getTaskName(), + data.status(), + data.timePassedMs()); + } } private void recordTime(String name, TriggerStatus status, Long timeMs) { @@ -57,7 +44,7 @@ private void recordTime(String name, TriggerStatus status, Long timeMs) { .register(meterRegistry); cache.put(key, timer); } - timer.record(timeMs, TimeUnit.MICROSECONDS); + timer.record(timeMs, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("Failed to update timer for {}", name, e); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java index 4f9b08fce..19a8dad1e 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java @@ -59,4 +59,9 @@ boolean hasValues(Collection elements) { public TriggerEntity getData() { return trigger.getData(); } + + @Override + public Long getId() { + return trigger.getId(); + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java index 39c7ae96a..d2785f952 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java @@ -85,16 +85,22 @@ public RunningTriggerEntity cancel(Exception e) { public void finishTriggerWithStatus(TriggerStatus status, Exception e) { this.data.setEnd(OffsetDateTime.now()); - this.data.updateRunningDuration(); this.data.setStatus(status); + this.data.updateRunningDuration(); + this.lastPing = null; - if (e != null) { + if (e == null) { + this.data.setExceptionName(null); + this.data.setLastException(null); + } else { this.data.setExceptionName(e.getClass().getName()); this.data.setLastException(ExceptionUtils.getStackTrace(e)); } } public RunningTriggerEntity runOn(String runningOn) { + this.data.setExceptionName(null); + this.data.setLastException(null); this.data.setStart(OffsetDateTime.now()); this.data.setEnd(null); this.data.setExecutionCount(data.getExecutionCount() + 1); @@ -108,7 +114,8 @@ public RunningTriggerEntity runOn(String runningOn) { public RunningTriggerEntity runAt(OffsetDateTime runAt) { data.setStatus(TriggerStatus.WAITING); data.setRunAt(runAt); - setRunningOn(null); + this.runningOn = null; + this.lastPing = null; return this; } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java index 3750998c9..de031ddf8 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java @@ -69,9 +69,11 @@ int markTriggersAsRunning( @Query(""" SELECT e FROM #{#entityName} e - WHERE e.lastPing < :lastPing + WHERE e.data.status = :status + AND e.lastPing < :lastPing """) List findTriggersLastPingAfter( + @Param("status") TriggerStatus status, @Param("lastPing") OffsetDateTime lastPing); @Query(""" diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/history/HistoryServiceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/history/HistoryServiceTest.java index c9a08228b..956f8df08 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/history/HistoryServiceTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/history/HistoryServiceTest.java @@ -47,24 +47,23 @@ void testReQueueTrigger() { @Test void testTriggerHistory() throws TimeoutException, InterruptedException { // GIVEN - final var trigger = Task3.ID.newUniqueTrigger("Hallo"); - triggerService.queue(trigger); + final var trigger = triggerService.queue(Task3.ID.newUniqueTrigger("Hallo")); persistentTaskTestService.runNextTrigger(); // WHEN - var triggers = subject.findAllDetailsForKey(trigger.key(), PageRequest.of(0, 100)).getContent(); + var triggers = subject.findAllDetailsForInstance(trigger.getId(), + PageRequest.of(0, 100)).getContent(); // AND assertThat(triggers).hasSize(3); - assertThat(triggers.get(0).getData().getStatus()).isEqualTo(TriggerStatus.SUCCESS); - assertThat(triggers.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING); - assertThat(triggers.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + assertThat(triggers.get(0).getStatus()).isEqualTo(TriggerStatus.SUCCESS); + assertThat(triggers.get(1).getStatus()).isEqualTo(TriggerStatus.RUNNING); + assertThat(triggers.get(2).getStatus()).isEqualTo(TriggerStatus.WAITING); } @RepeatedTest(3) void testTriggerHistoryTrx() { // GIVEN - final var trigger = Task3.ID.newUniqueTrigger("Hallo"); - persistentTaskService.queue(trigger); + final var trigger = persistentTaskService.queue(Task3.ID.newUniqueTrigger("Hallo")); // WHEN hibernateAsserts.reset(); schedulerService.triggerNextTasks().forEach(t -> { @@ -75,7 +74,6 @@ void testTriggerHistoryTrx() { // 2 to get the work done // 1 for the success history hibernateAsserts.assertTrxCount(3); - assertThat(subject.countTriggers(trigger.key())).isEqualTo(1); - assertThat(subject.findAllDetailsForKey(trigger.key()).getTotalElements()).isEqualTo(3); + assertThat(subject.countTriggers(trigger)).isEqualTo(1); } } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTest.java index c08da9a7d..49fd6526a 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTest.java @@ -129,7 +129,7 @@ void testRunOrQueue() throws Exception { asserts.awaitValue(Task3.NAME + "::Hallo"); persistentTaskTestService.awaitRunningTriggers(); - assertThat(persistentTaskService.getLastTriggerData(ref).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(ref).get().status()) .isEqualTo(TriggerStatus.SUCCESS); } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTransactionTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTransactionTest.java index e30445992..dc09f08b3 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTransactionTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerServiceTransactionTest.java @@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Pageable; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionTemplate; import org.sterl.spring.persistent_tasks.AbstractSpringTest; @@ -112,13 +113,13 @@ void testSaveNoTransactions() throws Exception { hibernateAsserts.assertTrxCount(4); assertThat(personRepository.count()).isOne(); // AND - var data = persistentTaskService.getLastDetailData(trigger.key()); - assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.SUCCESS); + var data = persistentTaskService.getLastTriggerHistory(trigger.getId()).get(); + assertThat(data.getStatus()).isEqualTo(TriggerStatus.SUCCESS); // AND - var history = historyService.findAllDetailsForKey(trigger.key()).getContent(); - assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.SUCCESS); - assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING); - assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + var history = historyService.findAllDetailsForInstance(trigger.getId(), Pageable.ofSize(10)).getContent(); + assertThat(history.get(0).getStatus()).isEqualTo(TriggerStatus.SUCCESS); + assertThat(history.get(1).getStatus()).isEqualTo(TriggerStatus.RUNNING); + assertThat(history.get(2).getStatus()).isEqualTo(TriggerStatus.WAITING); } @Test @@ -138,13 +139,13 @@ void testSaveTransactions() throws Exception { hibernateAsserts.assertTrxCount(3); assertThat(personRepository.count()).isOne(); // AND - var data = persistentTaskService.getLastDetailData(trigger.key()); - assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.SUCCESS); + var data = persistentTaskService.getLastTriggerHistory(trigger.getId()).get(); + assertThat(data.getStatus()).isEqualTo(TriggerStatus.SUCCESS); // AND - var history = historyService.findAllDetailsForKey(trigger.key()).getContent(); - assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.SUCCESS); - assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING); - assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + var history = historyService.findAllDetailsForInstance(trigger.getId(), Pageable.ofSize(10)).getContent(); + assertThat(history.get(0).getStatus()).isEqualTo(TriggerStatus.SUCCESS); + assertThat(history.get(1).getStatus()).isEqualTo(TriggerStatus.RUNNING); + assertThat(history.get(2).getStatus()).isEqualTo(TriggerStatus.WAITING); } @Test @@ -168,15 +169,16 @@ void test_fail_in_transaction() throws Exception { // 4. Update the status to failed and write the history hibernateAsserts.assertTrxCount(4); // AND - var data = persistentTaskService.getLastDetailData(trigger.key()); - assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.FAILED); + var history = persistentTaskService.getLastTriggerHistory(trigger.getId()).get(); + assertThat(history.getStatus()).isEqualTo(TriggerStatus.FAILED); + // AND assertThat(triggerService.get(trigger.getKey()).get().getRunningOn()).isNull(); assertThat(triggerService.get(trigger.getKey()).get().status()).isEqualTo(TriggerStatus.WAITING); // AND - var history = historyService.findAllDetailsForKey(trigger.key()).getContent(); - assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.FAILED); - assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING); - assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + var histories = historyService.findAllDetailsForInstance(trigger.getId(), Pageable.ofSize(10)).getContent(); + assertThat(histories.get(0).getStatus()).isEqualTo(TriggerStatus.FAILED); + assertThat(histories.get(1).getStatus()).isEqualTo(TriggerStatus.RUNNING); + assertThat(histories.get(2).getStatus()).isEqualTo(TriggerStatus.WAITING); } @Test @@ -187,7 +189,7 @@ void testRunOrQueueTransactions() throws Exception { // THEN 1 to save and 1 to start it and 1 for the history awaitHistoryThreads(); hibernateAsserts.assertTrxCount(3); - assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k1).get().status()) .isEqualTo(TriggerStatus.RUNNING); // WHEN @@ -200,7 +202,7 @@ void testRunOrQueueTransactions() throws Exception { // THEN assertThat(personRepository.count()).isEqualTo(1); // AND - assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k1).get().status()) .isEqualTo(TriggerStatus.SUCCESS); } @@ -211,9 +213,9 @@ void testRunOrQueueShowsRunning() throws Exception { var k2 = subject.runOrQueue(TriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build()); // WHEN - assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k1).get().status()) .isEqualTo(TriggerStatus.RUNNING); - assertThat(persistentTaskService.getLastTriggerData(k2).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k2).get().status()) .isEqualTo(TriggerStatus.RUNNING); COUNTDOWN.countDown(); @@ -222,9 +224,9 @@ void testRunOrQueueShowsRunning() throws Exception { // THEN assertThat(personRepository.count()).isEqualTo(2); // AND - assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k1).get().status()) .isEqualTo(TriggerStatus.SUCCESS); - assertThat(persistentTaskService.getLastTriggerData(k2).get().getStatus()) + assertThat(persistentTaskService.getLastTriggerData(k2).get().status()) .isEqualTo(TriggerStatus.SUCCESS); } @@ -241,13 +243,11 @@ void testRollbackAndRetry() throws Exception { awaitRunningTasks(); // THEN - var history = historyService.findAllDetailsForKey(key).getContent(); - assertThat(history.get(0).getData().getStatus()) - .isEqualTo(TriggerStatus.FAILED); - assertThat(history.get(1).getData().getStatus()) - .isEqualTo(TriggerStatus.RUNNING); - assertThat(history.get(2).getData().getStatus()) - .isEqualTo(TriggerStatus.WAITING); + var trigger = persistentTaskService.getLastTriggerData(key); + var history = historyService.findAllDetailsForInstance(trigger.get().getId(), Pageable.ofSize(10)).getContent(); + assertThat(history.get(0).getStatus()).isEqualTo(TriggerStatus.FAILED); + assertThat(history.get(1).getStatus()).isEqualTo(TriggerStatus.RUNNING); + assertThat(history.get(2).getStatus()).isEqualTo(TriggerStatus.WAITING); // WHEN sendError.set(false); @@ -262,7 +262,7 @@ void testRollbackAndRetry() throws Exception { private void assertExecutionCount(TriggerKey triggerKey, int count) throws InterruptedException, ExecutionException { var data = persistentTaskService.getLastTriggerData(triggerKey); assertThat(data).isPresent(); - assertThat(data.get().getExecutionCount()).isEqualTo(count); + assertThat(data.get().getData().getExecutionCount()).isEqualTo(count); } protected void awaitRunningTasks() throws TimeoutException, InterruptedException { diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/TaskFailoverTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/TaskFailoverTest.java index 87a0a1a04..28a0a6b81 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/TaskFailoverTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/scheduler/TaskFailoverTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.TriggerStatus; @@ -61,8 +62,9 @@ void rescheduleAbandonedTasksTest() throws Exception { assertThat(triggerService.countTriggers(TriggerStatus.WAITING)) .isEqualTo(1); // AND - var timeoutTrigger = historyService.findAllDetailsForKey(willTimeout.getKey()).getContent().getFirst(); - assertThat(timeoutTrigger.status()).isEqualTo(TriggerStatus.FAILED); - assertThat(timeoutTrigger.getData().getLastException()).contains("Trigger abandoned"); + var timeoutTrigger = historyService.findAllDetailsForInstance(willTimeout.getId(), + Pageable.ofSize(10)).getContent().getFirst(); + assertThat(timeoutTrigger.getStatus()).isEqualTo(TriggerStatus.FAILED); + assertThat(timeoutTrigger.getMessage()).contains("Trigger abandoned"); } } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerLifeCycleTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerLifeCycleTest.java index dee56b786..f99553ec2 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerLifeCycleTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerLifeCycleTest.java @@ -95,11 +95,12 @@ public void afterTriggerFailed(String state, Exception e) { } }); - trx.executeWithoutResult(trx -> { + trx.execute(trx -> { var t = subject.queue(taskId.newTrigger().waitForSignal(OffsetDateTime.now().minusSeconds(1)).build()); t.runOn("foo-bar-gone"); t.setLastPing(OffsetDateTime.now().minusDays(1)); t.getData().setExecutionCount(99); + return t; }); // WHEN diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerServiceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerServiceTest.java index 41c9e1dff..39c20fb99 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerServiceTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerServiceTest.java @@ -28,7 +28,6 @@ import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer.DeSerializationFailedException; import org.sterl.spring.persistent_tasks.trigger.event.TriggerAddedEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerCanceledEvent; -import org.sterl.spring.persistent_tasks.trigger.event.TriggerExpiredEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerResumedEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; @@ -430,6 +429,28 @@ void testRescheduleAbandonedTasks() { assertThat(rescheduledTasks.get(0).getKey()).isEqualTo(t1.getKey()); } + @Test + void testRescheduleAbandonedTasksOnlyRunning() { + // GIVEN + var now = OffsetDateTime.now(); + var t1 = new RunningTriggerEntity(new TriggerKey(UuidCreator.getTimeOrdered().toString(), "fooTask")) + .runOn("fooScheduler"); + t1.setLastPing(now.minusSeconds(60)); + triggerRepository.save(t1); + + var t2 = new RunningTriggerEntity( + new TriggerKey(UuidCreator.getTimeOrdered().toString(), "barTask")); + t2.setLastPing(now.minusSeconds(60)); + triggerRepository.save(t2); + + // WHEN + final var rescheduledTasks = subject.rescheduleAbandoned(now.minusSeconds(59)); + + // THEN + assertThat(rescheduledTasks).hasSize(1); + assertThat(rescheduledTasks.get(0).getKey()).isEqualTo(t1.getKey()); + } + @Test void testUnknownTriggersNoRetry() { // GIVEN @@ -441,8 +462,8 @@ void testUnknownTriggersNoRetry() { // WHEN var triggerData = persistentTaskService.getLastTriggerData(t.getKey()).get(); - assertThat(triggerData.getStatus()).isEqualTo(TriggerStatus.FAILED); - assertThat(triggerData.getExceptionName()).isEqualTo(IllegalStateException.class.getName()); + assertThat(triggerData.status()).isEqualTo(TriggerStatus.FAILED); + assertThat(triggerData.getData().getExceptionName()).isEqualTo(IllegalStateException.class.getName()); } @Test @@ -457,8 +478,8 @@ void testBadStateNoRetry() { // WHEN var triggerData = persistentTaskService.getLastTriggerData(t.getKey()).get(); - assertThat(triggerData.getStatus()).isEqualTo(TriggerStatus.FAILED); - assertThat(triggerData.getExceptionName()).isEqualTo(DeSerializationFailedException.class.getName()); + assertThat(triggerData.status()).isEqualTo(TriggerStatus.FAILED); + assertThat(triggerData.getData().getExceptionName()).isEqualTo(DeSerializationFailedException.class.getName()); // AND assertThat(events.stream(TriggerSuccessEvent.class).count()).isZero(); assertThat(events.stream(TriggerFailedEvent.class).count()).isOne(); diff --git a/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v6.xml b/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v6.xml new file mode 100644 index 000000000..6d2e2ae48 --- /dev/null +++ b/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v6.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/README.md b/example/README.md index 378f9fae0..308888515 100644 --- a/example/README.md +++ b/example/README.md @@ -5,3 +5,8 @@ - url: http://localhost:8080/task-ui - user: admin - password: admin + + +# Read metric by status + +- http://localhost:8080/actuator/metrics/persistent_tasks.task.failingBuildVehicleTask?tag=status:FAILED diff --git a/example/src/main/resources/application.yml b/example/src/main/resources/application.yml index a989a5159..a186a1d71 100644 --- a/example/src/main/resources/application.yml +++ b/example/src/main/resources/application.yml @@ -8,6 +8,8 @@ spring: persistent-tasks: max-threads: 1 + trigger-timeout: PT2M + poll-abandoned-triggers: 60 liquibase: change-log: classpath:db/changelog/db.changelog-master.xml diff --git a/ui/spt-ui-lib/lib/server-api.ts b/ui/spt-ui-lib/lib/server-api.ts index 25e4b92ae..2fbc4509c 100644 --- a/ui/spt-ui-lib/lib/server-api.ts +++ b/ui/spt-ui-lib/lib/server-api.ts @@ -3,7 +3,7 @@ export interface PagedModel { content: T[]; - page: PageMetadata; + page?: PageMetadata; } export interface SchedulerEntity { @@ -16,6 +16,17 @@ export interface SchedulerEntity { lastPing: string; } +export interface HistoryTrigger { + id: number; + instanceId: number; + key: TriggerKey; + createdTime: string; + start?: string; + executionCount: number; + status: TriggerStatus; + message?: string; +} + export interface TaskStatusHistoryOverview { taskName: string; status: TriggerStatus; @@ -32,21 +43,21 @@ export interface Trigger { id: number; instanceId: number; key: TriggerKey; - tag: string; - correlationId: string; - runningOn: string; + tag?: string; + correlationId?: string; + runningOn?: string; createdTime: string; runAt: string; - lastPing: string; - start: string; - end: string; + lastPing?: string; + start?: string; + end?: string; executionCount: number; priority: number; status: TriggerStatus; - runningDurationInMs: number; - state: any; - exceptionName: string; - lastException: string; + runningDurationInMs?: number; + state?: any; + exceptionName?: string; + lastException?: string; } export interface TriggerGroup { diff --git a/ui/spt-ui-lib/lib/shared/date.util.ts b/ui/spt-ui-lib/lib/shared/date.util.ts index 146b1119e..91f7c4e68 100644 --- a/ui/spt-ui-lib/lib/shared/date.util.ts +++ b/ui/spt-ui-lib/lib/shared/date.util.ts @@ -30,18 +30,15 @@ export function formatShortDateTime(inputDate?: string | Date): string { export function formatDateTime(inputDate?: string | Date): string { if (!inputDate) return ""; const date = inputDate instanceof Date ? inputDate : new Date(inputDate); - return new Intl.DateTimeFormat( - navigator.language || "en-US", - { - day: "2-digit", - month: "2-digit", - year: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, // Use 24-hour format - } - ).format(date); + return new Intl.DateTimeFormat(navigator.language || "en-US", { + day: "2-digit", + month: "2-digit", + year: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, // Use 24-hour format + }).format(date); } export function formatMs(ms?: number) { @@ -56,7 +53,7 @@ export function formatMs(ms?: number) { if (inS < 181) { return sign + inS + "s " + (ms - inS * 1000) + "ms"; } - + const inMin = Math.floor(inS / 60); if (inMin < 181) { return sign + inMin + "min " + (inS - inMin * 60) + "s"; @@ -73,4 +70,21 @@ export function runningSince(value?: string) { if (!value) return ""; const msRuntime = new Date().getTime() - new Date(value).getTime(); return `since ${formatMs(msRuntime)}`; -} \ No newline at end of file +} + +export function durationInSeconds( + start?: string | Date, + end?: string | Date +): number | undefined { + if (!start || !end) return undefined; + const startDate = typeof start === "string" ? new Date(start) : start; + const endDate = typeof end === "string" ? new Date(end) : end; + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return undefined; // invalid date input + } + + const seconds = (endDate.getTime() - startDate.getTime()) / 1000; + if (seconds < 100) return Math.round(seconds * 10) / 10; + return Math.round(seconds); +} diff --git a/ui/spt-ui-lib/lib/shared/http-request.ts b/ui/spt-ui-lib/lib/shared/http-request.ts index dd959cf8d..bb101c2cb 100644 --- a/ui/spt-ui-lib/lib/shared/http-request.ts +++ b/ui/spt-ui-lib/lib/shared/http-request.ts @@ -55,6 +55,7 @@ export const useServerObject = ( } else { console.error(requestUrl, e); setError(e); + throw e; } }) .finally(() => setIsLoading(false)); diff --git a/ui/spt-ui-lib/lib/shared/view/page.view.tsx b/ui/spt-ui-lib/lib/shared/view/page.view.tsx index 4d4930a59..417ffd413 100644 --- a/ui/spt-ui-lib/lib/shared/view/page.view.tsx +++ b/ui/spt-ui-lib/lib/shared/view/page.view.tsx @@ -15,11 +15,11 @@ const PageView: React.FC = ({ }) => { if (!data || isLoading) return ; - const currentPage = data?.page.number ?? 0; - const totalPages = data?.page.totalPages ?? 0; - const totalElements = data?.page.totalElements ?? 0; - const pageSize = data?.page.size ?? 0; - const contentLength = data?.content?.length ?? 0; + const currentPage = data.page?.number ?? 0; + const totalPages = data.page?.totalPages ?? 0; + const totalElements = data.page?.totalElements ?? 0; + const pageSize = data.page?.size ?? 0; + const contentLength = data.content?.length ?? 0; const handlePrevClick = () => { if (currentPage > 0) { diff --git a/ui/spt-ui-lib/lib/shared/view/trigger-history-list.view.tsx b/ui/spt-ui-lib/lib/shared/view/trigger-history-list.view.tsx index 6602ec652..00f8d7e9a 100644 --- a/ui/spt-ui-lib/lib/shared/view/trigger-history-list.view.tsx +++ b/ui/spt-ui-lib/lib/shared/view/trigger-history-list.view.tsx @@ -1,13 +1,13 @@ -import { type Trigger } from "@lib/server-api"; -import { formatDateTime, formatMs } from "@lib/shared/date.util"; +import { type HistoryTrigger, type PagedModel } from "@lib/server-api"; +import { formatDateTime } from "@lib/shared/date.util"; import { useEffect } from "react"; import { Col, ListGroup, Row } from "react-bootstrap"; import { useServerObject } from "../http-request"; import HttpRequestView from "./http-request.view"; -import RunningTriggerStatusView from "./running-trigger-status.view"; +import TriggerStatusView from "./trigger-status.view"; const TriggerHistoryListView = ({ instanceId }: { instanceId: number }) => { - const triggerHistory = useServerObject( + const triggerHistory = useServerObject>( "/spring-tasks-api/history/instance/" ); const doGet = triggerHistory.doGet; @@ -18,18 +18,26 @@ const TriggerHistoryListView = ({ instanceId }: { instanceId: number }) => { request={triggerHistory} render={(history) => ( - {history.map((t) => ( + {history.content.map((t) => ( - - - + + + #{t.executionCount} - + + + + {formatDateTime(t.createdTime)} + + + {t.message} - {formatDateTime(t.createdTime)} - execution: {t.executionCount} - {formatMs(t.runningDurationInMs)} ))} diff --git a/ui/spt-ui-lib/lib/shared/view/trigger-status-icon.view.tsx b/ui/spt-ui-lib/lib/shared/view/trigger-status-icon.view.tsx index 6163d91be..256d766df 100644 --- a/ui/spt-ui-lib/lib/shared/view/trigger-status-icon.view.tsx +++ b/ui/spt-ui-lib/lib/shared/view/trigger-status-icon.view.tsx @@ -29,13 +29,22 @@ const getIcon = (status: TriggerStatus) => { const getVariant = (trigger: Trigger): string => { if (["FAILED", "EXPIRED_SIGNAL"].includes(trigger.status)) return "danger"; if (trigger.status === "CANCELED") return "secondary"; - if (trigger.status === "SUCCESS") return "success"; if (trigger.executionCount > 1) return "warning"; + if (trigger.status === "SUCCESS") return "success"; if (trigger.status === "RUNNING") return "primary"; if (trigger.status === "WAITING") return "secondary"; return ""; }; +const getStatusText = (trigger: Trigger): string => { + if (trigger.executionCount > 1 && trigger.status === "RUNNING") { + return trigger.status + " for " + trigger.executionCount + ". time"; + } else if (trigger.executionCount > 1 && trigger.status === "WAITING") { + return trigger.status + " for " + trigger.executionCount + ". retry"; + } + return trigger.status.replace(/_/g, " "); +}; + const TriggerStatusIcon = ({ trigger }: { trigger: Trigger }) => { const variant = getVariant(trigger); const icon = getIcon(trigger.status); @@ -43,9 +52,7 @@ const TriggerStatusIcon = ({ trigger }: { trigger: Trigger }) => { return ( {icon} - - {trigger.status.replace(/_/g, " ")} - + {getStatusText(trigger)} ); }; diff --git a/ui/spt-ui-lib/lib/shared/view/trigger-summary.view.tsx b/ui/spt-ui-lib/lib/shared/view/trigger-summary.view.tsx deleted file mode 100644 index 075a9caeb..000000000 --- a/ui/spt-ui-lib/lib/shared/view/trigger-summary.view.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { Trigger } from "@lib/server-api"; -import { Badge, Col, Row, type ColProps } from "react-bootstrap"; -import LabeledText from "./labled-text.view"; -import { formatMs, formatShortDateTime, runningSince } from "../date.util"; - -const TriggerSummaryView = ({ - trigger, - col = { md: 3, xs: 6 }, -}: { - trigger: Trigger; - col?: ColProps; -}) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default TriggerSummaryView; - -const TriggerExecutiomView = ({ trigger }: { trigger: Trigger }) => { - if (trigger.runningOn) { - return ( - - ); - } - if (trigger.status == "WAITING" && trigger.runAt) { - return ( - - ); - } - return ( - - 1 ? "warning" : "success"} - > - {trigger.executionCount} - - - {formatMs(trigger.runningDurationInMs)} - - - } - /> - ); -}; diff --git a/ui/spt-ui-lib/lib/shared/view/trigger.view.tsx b/ui/spt-ui-lib/lib/shared/view/trigger.view.tsx index a9d0528d2..4c3fe30ae 100644 --- a/ui/spt-ui-lib/lib/shared/view/trigger.view.tsx +++ b/ui/spt-ui-lib/lib/shared/view/trigger.view.tsx @@ -71,12 +71,27 @@ const TriggerView = ({ value={formatShortDateTime(trigger.end)} /> + + + + + + + + + diff --git a/ui/spt-ui-lib/src/App.tsx b/ui/spt-ui-lib/src/App.tsx index f56413a13..28811bec1 100644 --- a/ui/spt-ui-lib/src/App.tsx +++ b/ui/spt-ui-lib/src/App.tsx @@ -15,11 +15,20 @@ function App() { content: [], } ); + const triggerHistory = useServerObject>( + "/spring-tasks-api/history?size=15", + { + page: { number: 0, size: 0, totalElements: 0, totalPages: 0 }, + content: [], + } + ); useEffect(triggers.doGet, []); + useEffect(triggerHistory.doGet, []); return ( + );