diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98b2bf362..88f91953d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,7 +80,6 @@ jobs: services: postgres: image: postgres - options: --name pg-container env: POSTGRES_USER: sa POSTGRES_PASSWORD: veryStrong123 diff --git a/RUN_AND_BUILD.md b/RUN_AND_BUILD.md index 75c7c8b29..03814adbf 100644 --- a/RUN_AND_BUILD.md +++ b/RUN_AND_BUILD.md @@ -5,7 +5,7 @@ mvn versions:set -DnewVersion=1.6.0-SNAPSHOT -DgenerateBackupPoms=false ## postgres -docker run --name pg-container -e POSTGRES_USER=sa -e POSTGRES_PASSWORD=veryStrong123 -p 5432:5432 -d postgres +docker run -e POSTGRES_USER=sa -e POSTGRES_PASSWORD=veryStrong123 -p 5432:5432 -d postgres ## azure-sql-edge diff --git a/core/pom.xml b/core/pom.xml index 5717b495e..32efc07bc 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.7.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml @@ -25,6 +25,21 @@ org.springframework.boot spring-boot-starter-data-jpa + + + com.querydsl + querydsl-apt + ${querydsl.version} + jakarta + provided + + + com.querydsl + querydsl-jpa + ${querydsl.version} + jakarta + + org.springframework.boot spring-boot-starter-validation @@ -38,6 +53,12 @@ liquibase-core + + com.github.f4b6a3 + uuid-creator + 6.1.1 + + org.springframework.boot spring-boot-devtools @@ -52,11 +73,6 @@ test - - org.liquibase - liquibase-core - test - com.h2database h2 @@ -186,12 +202,16 @@ org.springframework.data.web.PagedModel - - org.sterl.spring.persistent_tasks.scheduler.entity.SchedulerEntity + org.sterl.spring.persistent_tasks.scheduler.entity.SchedulerEntity - org.sterl.spring.persistent_tasks.api.** + org.sterl.spring.persistent_tasks.api.* + + java.lang.Class + org.sterl.spring.persistent_tasks.api.QTriggerKey + java.io.Serializable + ../ui/src/server-api.d.ts module 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 5382048e7..63824164f 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 @@ -7,23 +7,24 @@ import java.util.List; import java.util.Optional; +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; import org.springframework.data.domain.Sort.Direction; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; +import org.sterl.spring.persistent_tasks.api.TriggerSearch; 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.TriggerHistoryLastStateEntity; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; import org.sterl.spring.persistent_tasks.scheduler.SchedulerService; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +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.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.RequiredArgsConstructor; @@ -40,14 +41,14 @@ public class PersistentTaskService { private final HistoryService historyService; /** - * Returns the last known {@link TriggerData} to a given key. First running triggers are checked. + * Returns the last known {@link TriggerEntity} to a given key. First running triggers are checked. * Maybe out of the history event from a retry execution of the very same id. * * @param key the {@link TriggerKey} to look for - * @return the {@link TriggerData} to the {@link TriggerKey} + * @return the {@link TriggerEntity} to the {@link TriggerKey} */ - public Optional getLastTriggerData(TriggerKey key) { - final Optional trigger = triggerService.get(key); + public Optional getLastTriggerData(TriggerKey key) { + final Optional trigger = triggerService.get(key); if (trigger.isEmpty()) { var history = historyService.findLastKnownStatus(key); if (history.isPresent()) { @@ -59,7 +60,7 @@ public Optional getLastTriggerData(TriggerKey key) { } } - public Optional getLastDetailData(TriggerKey key) { + public Optional getLastDetailData(TriggerKey key) { var data = historyService.findAllDetailsForKey(key, Pageable.ofSize(1)); if (data.isEmpty()) { return Optional.empty(); @@ -85,14 +86,14 @@ void queue(TriggerTaskCommand event) { */ @Transactional(timeout = 10) @NonNull - public List queue(Collection> triggers) { + public List queue(Collection> triggers) { if (triggers == null || triggers.isEmpty()) { return Collections.emptyList(); } return triggers.stream() // .map(t -> triggerService.queue(t)) // - .map(TriggerEntity::getKey) // + .map(RunningTriggerEntity::getKey) // .toList(); } /** @@ -104,7 +105,7 @@ public List queue(Collection TriggerKey queue(AddTriggerRequest trigger) { + public TriggerKey queue(TriggerRequest trigger) { return triggerService.queue(trigger).getKey(); } @@ -114,7 +115,7 @@ public TriggerKey queue(AddTriggerRequest trigger) { * @return the reference to the {@link TriggerKey} */ public TriggerKey runOrQueue( - AddTriggerRequest triggerRequest) { + TriggerRequest triggerRequest) { if (schedulerService.isPresent()) { schedulerService.get().runOrQueue(triggerRequest); } else { @@ -128,42 +129,41 @@ public TriggerKey runOrQueue( * Data is limited to overall 300 elements. * * @param correlationId the id to search for - * @return the found {@link TriggerData} sorted by create time ASC + * @return the found {@link TriggerEntity} sorted by create time ASC */ @Transactional(readOnly = true, timeout = 5) - public List findAllTriggerByCorrelationId(String correlationId) { + public List findAllTriggerByCorrelationId(String correlationId) { + if (StringUtils.isAllBlank(correlationId)) return Collections.emptyList(); + + final var search = TriggerSearch.byCorrelationId(correlationId); - var running = triggerService.findTriggerByCorrelationId(correlationId, Pageable.ofSize(100)) - .stream().map(TriggerEntity::getData) + final var running = triggerService.searchTriggers(search, PageRequest.of(0, 100, TriggerSearch.DEFAULT_SORT)) + .stream().map(RunningTriggerEntity::getData) .toList(); - var done = historyService.findTriggerByCorrelationId(correlationId, Pageable.ofSize(200)) - .stream().map(TriggerHistoryLastStateEntity::getData) + final var done = historyService.searchTriggers(search, PageRequest.of(0, 200, TriggerSearch.DEFAULT_SORT)) + .stream().map(CompletedTriggerEntity::getData) .toList(); - var result = new ArrayList(running.size() + done.size()); + final var result = new ArrayList(running.size() + done.size()); result.addAll(done); result.addAll(running); return result; } - - /** - * Returns the first info to a trigger based on the correlationId. - * - * @param correlationId the id to search for - * @return the found {@link TriggerData} - */ + @Transactional(readOnly = true, timeout = 5) - public Optional findLastTriggerByCorrelationId(String correlationId) { - final var page = PageRequest.of(0, 1, Sort.by(Direction.DESC, "data.createdTime")); - var result = triggerService.findTriggerByCorrelationId(correlationId, page) - .stream().map(TriggerEntity::getData) - .toList(); + public Optional findLastTriggerByCorrelationId(String correlationId) { + final var page = PageRequest.of(0, 1, TriggerSearch.sortByCreatedTime(Direction.DESC)); + final var search = TriggerSearch.byCorrelationId(correlationId); + + var result = triggerService.searchTriggers(search, page) + .stream().map(RunningTriggerEntity::getData) + .toList(); if (result.isEmpty()) { - result = historyService.findTriggerByCorrelationId(correlationId, page) - .stream().map(TriggerHistoryLastStateEntity::getData) - .toList(); + result = historyService.searchTriggers(search, page) + .stream().map(CompletedTriggerEntity::getData) + .toList(); } return result.isEmpty() ? Optional.empty() : Optional.of(result.getFirst()); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java index 0d20588b9..e5a353e56 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java @@ -20,7 +20,7 @@ public TriggerBuilder newTrigger(T state) { return new TriggerBuilder<>(this).state(state); } - public AddTriggerRequest newUniqueTrigger(T state) { + public TriggerRequest newUniqueTrigger(T state) { return new TriggerBuilder<>(this).state(state).build(); } @@ -34,9 +34,11 @@ public static class TriggerBuilder { private final TaskId taskId; private String id; private String correlationId; + private String tag; + private TriggerStatus status = TriggerStatus.WAITING; private T state; private OffsetDateTime when = OffsetDateTime.now(); - private int priority = AddTriggerRequest.DEFAULT_PRIORITY; + private int priority = TriggerRequest.DEFAULT_PRIORITY; public static TriggerBuilder newTrigger(String name) { return new TriggerBuilder<>(new TaskId(name)); @@ -44,9 +46,12 @@ public static TriggerBuilder newTrigger(String name) public static TriggerBuilder newTrigger(String name, T state) { return new TriggerBuilder<>(new TaskId(name)).state(state); } - public AddTriggerRequest build() { + public static TriggerBuilder newTrigger(TriggerKey key, T state) { + return new TriggerBuilder<>(new TaskId(key.getTaskName())).id(key.getId()).state(state); + } + public TriggerRequest build() { var key = TriggerKey.of(id, taskId); - return new AddTriggerRequest<>(key, state, when, priority, correlationId); + return new TriggerRequest<>(key, status, state, when, priority, correlationId, tag); } /** * The ID of this task, same queued ids are replaced. @@ -63,6 +68,10 @@ public TriggerBuilder correlationId(String correlationId) { this.correlationId = correlationId; return this; } + public TriggerBuilder tag(String tag) { + this.tag = tag; + return this; + } public TriggerBuilder state(T state) { this.state = state; return this; @@ -86,11 +95,24 @@ public TriggerBuilder when(OffsetDateTime when) { } public TriggerBuilder runAt(OffsetDateTime when) { this.when = when; + this.status = TriggerStatus.WAITING; return this; } + /** + * synonym for {@link #runAt(OffsetDateTime)} + */ public TriggerBuilder runAfter(Duration duration) { runAt(OffsetDateTime.now().plus(duration)); return this; } + /** + * Creates a trigger which waits for an external signal and + * will run into {@link TriggerStatus#EXPIRED_SIGNAL} if no signal happens. + */ + public TriggerBuilder waitForSignal(OffsetDateTime timeout) { + this.when = timeout; + this.status = TriggerStatus.AWAITING_SIGNAL; + return this; + } } } 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 e235762e7..719ab4274 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 @@ -15,6 +15,8 @@ public class Trigger { /** the business key which is unique it is combination for triggers but not the history! */ private TriggerKey key; + private String tag; + private String correlationId; private String runningOn; diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java index 6fe4e83c1..52ea4199c 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java @@ -2,7 +2,6 @@ import java.io.Serializable; import java.time.OffsetDateTime; -import java.util.UUID; import org.springframework.lang.Nullable; @@ -29,7 +28,7 @@ public class TriggerKey implements Serializable { private String taskName; public static TriggerKey of(@Nullable String id, TaskId taskId) { - return new TriggerKey(id == null ? UUID.randomUUID().toString() : id, taskId.name()); + return new TriggerKey(id, taskId.name()); } public TaskId toTaskId() { @@ -40,30 +39,29 @@ public TaskId toTaskId() { * Builds a trigger for the given persistentTask name */ public TriggerKey(String taskName) { - id = UUID.randomUUID().toString(); this.taskName = taskName; } /** * Just triggers the given persistentTask to be executed using null as state. */ - public AddTriggerRequest newTrigger(TaskId taskId) { + public TriggerRequest newTrigger(TaskId taskId) { return newTrigger(taskId, null); } - public AddTriggerRequest newTrigger(TaskId taskId, T state) { - return newTrigger(UUID.randomUUID().toString(), taskId, state); + public TriggerRequest newTrigger(TaskId taskId, T state) { + return newTrigger(null, taskId, state); } - public AddTriggerRequest newTrigger(String id, TaskId taskId, T state) { + public TriggerRequest newTrigger(String id, TaskId taskId, T state) { return newTrigger(id, taskId, state, OffsetDateTime.now()); } - public AddTriggerRequest newTrigger(String id, TaskId taskId, T state, OffsetDateTime when) { + public TriggerRequest newTrigger(String id, TaskId taskId, T state, OffsetDateTime when) { return taskId.newTrigger() // .id(id) // .state(state) // .when(when) // - .build(); + .build(); // } } \ No newline at end of file diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerRequest.java similarity index 78% rename from core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerRequest.java index 3352eba08..bc3eae394 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerRequest.java @@ -10,19 +10,21 @@ * For any registered persistentTask a persistentTask trigger represent one unit of work, executing this persistentTask once. * @param state type which has to be of {@link Serializable} */ -public record AddTriggerRequest( +public record TriggerRequest( TriggerKey key, + TriggerStatus status, T state, OffsetDateTime runtAt, int priority, - String correlationId) { + String correlationId, + String tag) { @SuppressWarnings("unchecked") public TaskId taskId() { return (TaskId)key.toTaskId(); } - public Collection> toList() { + public Collection> toList() { return Collections.singleton(this); } 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 new file mode 100644 index 000000000..7e0b740fd --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerSearch.java @@ -0,0 +1,67 @@ +package org.sterl.spring.persistent_tasks.api; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.sterl.spring.persistent_tasks.shared.StringHelper; + +import lombok.Data; + +@Data +public class TriggerSearch { + private String search; + private String keyId; + private String taskName; + private String correlationId; + private TriggerStatus status; + private String tag; + + public boolean hasValue() { + return StringHelper.hasValue(search) + || StringHelper.hasValue(keyId) + || StringHelper.hasValue(taskName) + || StringHelper.hasValue(tag) + || status != null; + } + + + 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) { + search.setKeyId(trigger.key().getId()); + search.setTaskName(trigger.key().getTaskName()); + } + + if (trigger.correlationId() != null) search.setCorrelationId(trigger.correlationId()); + if (trigger.tag() != null) search.setTag(trigger.tag()); + if (trigger.status() != null) search.setStatus(trigger.status()); + + return search; + } + + /** create time ASC */ + public static final Sort DEFAULT_SORT = sortByCreatedTime(Direction.ASC); + + 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()) { + result = PageRequest.of(page.getPageNumber(), page.getPageSize(), DEFAULT_SORT); + } + return result; + } + +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java index 5f383df03..ac46a4220 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java @@ -4,12 +4,14 @@ import java.util.Set; public enum TriggerStatus { + AWAITING_SIGNAL, WAITING, RUNNING, SUCCESS, FAILED, - CANCELED + CANCELED, + EXPIRED_SIGNAL ; - public static final Set ACTIVE_STATES = EnumSet.of(WAITING, RUNNING); - public static final Set END_STATES = EnumSet.of(SUCCESS, FAILED, CANCELED); + public static final Set ACTIVE_STATES = EnumSet.of(AWAITING_SIGNAL, WAITING, RUNNING); + public static final Set END_STATES = EnumSet.of(SUCCESS, FAILED, CANCELED, EXPIRED_SIGNAL); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java index fbdc85d72..52bfd7783 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java @@ -5,14 +5,14 @@ import java.util.Collection; import java.util.Collections; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TaskId.TriggerBuilder; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; /** * An event to trigger one or multiple persistentTask executions */ public record TriggerTaskCommand( - Collection> triggers) implements PersistentTasksEvent { + Collection> triggers) implements PersistentTasksEvent { public int size() { return triggers == null ? 0 : triggers.size(); @@ -33,16 +33,16 @@ public static TriggerTaskCommand of(String name, T s .build())); } - public static TriggerTaskCommand of(Collection> triggers) { + public static TriggerTaskCommand of(Collection> triggers) { return new TriggerTaskCommand<>(triggers); } - public static TriggerTaskCommand of(AddTriggerRequest trigger) { + public static TriggerTaskCommand of(TriggerRequest trigger) { return new TriggerTaskCommand<>(Collections.singleton(trigger)); } @SafeVarargs - public static TriggerTaskCommand of(AddTriggerRequest... triggers) { + public static TriggerTaskCommand of(TriggerRequest... triggers) { return new TriggerTaskCommand<>(Arrays.asList(triggers)); } } 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 f2863a794..11c7090ca 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 @@ -13,13 +13,14 @@ import org.springframework.lang.Nullable; import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.api.event.TriggerTaskCommand; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; +import org.sterl.spring.persistent_tasks.history.model.QCompletedTriggerEntity; import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository; -import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository; -import org.sterl.spring.persistent_tasks.shared.StringHelper; +import org.sterl.spring.persistent_tasks.history.repository.CompletedTriggerRepository; import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService; import lombok.RequiredArgsConstructor; @@ -27,27 +28,27 @@ @TransactionalService @RequiredArgsConstructor public class HistoryService { - private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository; + private final CompletedTriggerRepository completedTriggerRepository; private final TriggerHistoryDetailRepository triggerHistoryDetailRepository; private final ApplicationEventPublisher applicationEventPublisher; - public Optional findStatus(long triggerId) { - return triggerHistoryLastStateRepository.findById(triggerId); + public Optional findStatus(long triggerId) { + return completedTriggerRepository.findById(triggerId); } - public Optional findLastKnownStatus(TriggerKey triggerKey) { + public Optional findLastKnownStatus(TriggerKey triggerKey) { final var page = PageRequest.of(0, 1).withSort(Direction.DESC, "data.createdTime", "id"); - final var result = triggerHistoryLastStateRepository.listKnownStatusFor(triggerKey, page); + final var result = completedTriggerRepository.listKnownStatusFor(triggerKey, page); return result.isEmpty() ? Optional.empty() : Optional.of(result.getContent().get(0)); } public void deleteAll() { - triggerHistoryLastStateRepository.deleteAllInBatch(); + completedTriggerRepository.deleteAllInBatch(); triggerHistoryDetailRepository.deleteAllInBatch(); } public void deleteAllOlderThan(OffsetDateTime age) { - triggerHistoryLastStateRepository.deleteOlderThan(age); + completedTriggerRepository.deleteOlderThan(age); triggerHistoryDetailRepository.deleteOlderThan(age); } @@ -58,20 +59,20 @@ public long countTriggers(TriggerStatus status) { return triggerHistoryDetailRepository.countByStatus(status); } - public List findAllDetailsForInstance(long instanceId) { + public List findAllDetailsForInstance(long instanceId) { return triggerHistoryDetailRepository.findAllByInstanceId(instanceId); } - public Page findAllDetailsForKey(TriggerKey key) { + public Page findAllDetailsForKey(TriggerKey key) { return findAllDetailsForKey(key, PageRequest.of(0, 100)); } - public Page findAllDetailsForKey(TriggerKey key, Pageable page) { + public Page findAllDetailsForKey(TriggerKey key, Pageable page) { page = applyDefaultSortIfNeeded(page); return triggerHistoryDetailRepository.listKnownStatusFor(key, page); } public Optional reQueue(Long id, OffsetDateTime runAt) { - final var lastState = triggerHistoryLastStateRepository.findById(id); + final var lastState = completedTriggerRepository.findById(id); if (lastState.isEmpty()) return Optional.empty(); final var data = lastState.get().getData(); @@ -87,7 +88,7 @@ public Optional reQueue(Long id, OffsetDateTime runAt) { } public long countTriggers(TriggerKey key) { - return triggerHistoryLastStateRepository.countByKey(key); + return completedTriggerRepository.countByKey(key); } /** @@ -97,16 +98,21 @@ public long countTriggers(TriggerKey key) { * @param page page informations * @return the found data, looking only the last states */ - public Page findTriggerState( - @Nullable TriggerKey key, @Nullable TriggerStatus status, Pageable page) { + public Page searchTriggers( + @Nullable TriggerSearch search, Pageable page) { page = applyDefaultSortIfNeeded(page); - if (key == null && status == null) { - return triggerHistoryLastStateRepository.findAll(page); + Page result; + + if (search != null && search.hasValue()) { + var p = completedTriggerRepository.buildSearch( + QCompletedTriggerEntity.completedTriggerEntity.data, + search); + result = completedTriggerRepository.findAll(p, page); + } else { + result = completedTriggerRepository.findAll(page); } - final var id = StringHelper.applySearchWildCard(key); - final var name = key == null ? null : key.getTaskName(); - return triggerHistoryLastStateRepository.findAll(id, name, status, page); + return result; } private Pageable applyDefaultSortIfNeeded(Pageable page) { @@ -118,10 +124,6 @@ private Pageable applyDefaultSortIfNeeded(Pageable page) { } public List taskStatusHistory() { - return triggerHistoryLastStateRepository.listTriggerStatus(); - } - - public List findTriggerByCorrelationId(String correlationId, Pageable page) { - return triggerHistoryLastStateRepository.findByCorrelationId(correlationId, page); + return completedTriggerRepository.listTriggerStatus(); } } 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 27e4d7fa1..abcc6ee79 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 @@ -7,13 +7,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import org.sterl.spring.persistent_tasks.scheduler.config.ConditionalSchedulerServiceByProperty; +import org.sterl.spring.persistent_tasks.shared.TimersEnabled; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -@ConditionalSchedulerServiceByProperty +@TimersEnabled @Service @RequiredArgsConstructor @Slf4j 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 896469b43..da36fc5d3 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 @@ -2,19 +2,19 @@ import org.springframework.lang.NonNull; import org.sterl.spring.persistent_tasks.api.Trigger; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity; +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; import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; interface HistoryConverter { - enum FromLastTriggerStateEntity implements ExtendetConvert { + enum FromLastTriggerStateEntity implements ExtendetConvert { INSTANCE; @NonNull @Override - public Trigger convert(@NonNull TriggerHistoryLastStateEntity source) { + public Trigger convert(@NonNull CompletedTriggerEntity source) { var result = ToTrigger.INSTANCE.convert(source); result.setId(source.getId()); result.setInstanceId(source.getId()); @@ -22,12 +22,12 @@ public Trigger convert(@NonNull TriggerHistoryLastStateEntity source) { } } - enum FromTriggerStateDetailEntity implements ExtendetConvert { + enum FromTriggerStateDetailEntity implements ExtendetConvert { INSTANCE; @NonNull @Override - public Trigger convert(@NonNull TriggerHistoryDetailEntity source) { + public Trigger convert(@NonNull HistoryTriggerEntity source) { var result = ToTrigger.INSTANCE.convert(source); result.setId(source.getId()); result.setInstanceId(source.getInstanceId()); 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 551b7c05a..0a666b333 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 @@ -3,7 +3,6 @@ import java.time.OffsetDateTime; import java.util.List; -import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PagedModel; @@ -12,12 +11,11 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.api.TriggerStatus; +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; @@ -43,14 +41,11 @@ public List taskStatusHistory() { @GetMapping("history") public PagedModel list( - @RequestParam(name = "id", required = false) String id, - @RequestParam(name = "taskName", required = false) String taskName, - @RequestParam(name = "status", required = false) TriggerStatus status, + TriggerSearch search, @PageableDefault(size = 100) Pageable page) { - var key = new TriggerKey(StringUtils.trimToNull(id), StringUtils.trimToNull(taskName)); return FromLastTriggerStateEntity.INSTANCE.toPage( // - historyService.findTriggerState(key, status, page)); + historyService.searchTriggers(search, page)); } @PostMapping("history/{id}/re-run") 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 635aa3a16..ace10dbdc 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 @@ -7,11 +7,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository; -import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.history.repository.CompletedTriggerRepository; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalCompontant; import org.sterl.spring.persistent_tasks.trigger.event.TriggerLifeCycleEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent; @@ -24,7 +24,7 @@ @Slf4j public class TriggerHistoryComponent { - private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository; + private final CompletedTriggerRepository completedTriggerRepository; private final TriggerHistoryDetailRepository triggerHistoryDetailRepository; // we have to ensure to run in an own transaction @@ -53,15 +53,15 @@ void onPersistentTaskEvent(TriggerLifeCycleEvent e) { execute(e.id(), e.data(), e.isDone()); } - public void execute(final long triggerId, final TriggerData data, boolean isDone) { + public void execute(final long triggerId, final TriggerEntity data, boolean isDone) { if (isDone) { - final var state = new TriggerHistoryLastStateEntity(); + final var state = new CompletedTriggerEntity(); state.setId(triggerId); state.setData(data.copy()); - triggerHistoryLastStateRepository.save(state); + completedTriggerRepository.save(state); } - var detail = new TriggerHistoryDetailEntity(); + var detail = new HistoryTriggerEntity(); detail.setInstanceId(triggerId); detail.setData(data.toBuilder() .state(null) diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/config/TriggerHistoryConfig.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/config/TriggerHistoryConfig.java index a81b33f71..9f50b581a 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/config/TriggerHistoryConfig.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/config/TriggerHistoryConfig.java @@ -2,14 +2,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @Configuration public class TriggerHistoryConfig { @Bean - TaskExecutor triggerHistoryExecutor() { + ThreadPoolTaskExecutor triggerHistoryExecutor() { var taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(1); taskExecutor.setMaxPoolSize(4); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryLastStateEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java similarity index 83% rename from core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryLastStateEntity.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java index 335d8a72f..0819fc916 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryLastStateEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/CompletedTriggerEntity.java @@ -3,8 +3,8 @@ import java.time.OffsetDateTime; import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -20,19 +20,20 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "pt_trigger_history_last_states", indexes = { +@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"), }) @Data @NoArgsConstructor @Builder(toBuilder = true) @AllArgsConstructor @EqualsAndHashCode(of = "id") -public class TriggerHistoryLastStateEntity implements HasTriggerData { +public class CompletedTriggerEntity implements HasTrigger { @Column(updatable = false) @Id @@ -40,7 +41,7 @@ public class TriggerHistoryLastStateEntity implements HasTriggerData { @Embedded @NotNull - private TriggerData data; + private TriggerEntity data; public TriggerKey getKey() { return data.getKey(); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryDetailEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java similarity index 61% rename from core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryDetailEntity.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java index 2d12e355d..9e226a129 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/TriggerHistoryDetailEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/model/HistoryTriggerEntity.java @@ -3,8 +3,8 @@ import java.time.OffsetDateTime; import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -22,23 +22,24 @@ import lombok.NoArgsConstructor; /** - * Just a copy of the trigger status but without any data/state. + * The history of the {@link TriggerEntity} with the states. */ @Entity -@Table(name = "pt_trigger_history_details", indexes = { - @Index(name = "idx_pt_triggers_history_instance_id", columnList = "instance_id"), - @Index(name = "idx_pt_triggers_history_task_name", columnList = "task_name"), - @Index(name = "idx_pt_triggers_history_trigger_id", columnList = "trigger_id"), - @Index(name = "idx_pt_triggers_history_status", columnList = "status"), - @Index(name = "idx_pt_triggers_history_created_time", columnList = "created_time"), - @Index(name = "idx_pt_trigger_history_details_correlation_id", columnList = "correlation_id"), +@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"), }) @Data @NoArgsConstructor @Builder(toBuilder = true) @AllArgsConstructor @EqualsAndHashCode(of = "id") -public class TriggerHistoryDetailEntity implements HasTriggerData { +public class HistoryTriggerEntity implements HasTrigger { @GeneratedValue(generator = "seq_pt_trigger_history_details", strategy = GenerationType.SEQUENCE) @Column(updatable = false) @@ -53,7 +54,7 @@ public class TriggerHistoryDetailEntity implements HasTriggerData { @Embedded @NotNull - private TriggerData data; + private TriggerEntity data; public TriggerKey getKey() { return data.getKey(); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java similarity index 82% rename from core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepository.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java index 0f0afcf25..46a9e96da 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepository.java @@ -4,9 +4,9 @@ import org.springframework.data.jpa.repository.Query; import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; -public interface TriggerHistoryLastStateRepository extends HistoryTriggerRepository { +public interface CompletedTriggerRepository extends HistoryTriggerRepository { @Query(""" SELECT new org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview( 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 index 86e27fc15..37d62d881 100644 --- 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 @@ -6,11 +6,11 @@ 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.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.repository.TriggerDataRepository; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.repository.TriggerRepository; @NoRepositoryBean -public interface HistoryTriggerRepository extends TriggerDataRepository { +public interface HistoryTriggerRepository extends TriggerRepository { @Query(""" SELECT e FROM #{#entityName} e 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 0f2f42ab7..2ba576f1d 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 @@ -4,9 +4,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity; +import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; -public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository { +public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository { @Query(""" SELECT e @@ -14,5 +14,5 @@ public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository WHERE e.instanceId = :instanceId ORDER BY e.id DESC """) - List findAllByInstanceId(@Param("instanceId") long instanceId); + List findAllByInstanceId(@Param("instanceId") long instanceId); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerService.java index 8843f6bc7..dc0baa496 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerService.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerService.java @@ -15,15 +15,15 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.support.TransactionTemplate; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.scheduler.component.EditSchedulerStatusComponent; import org.sterl.spring.persistent_tasks.scheduler.component.RunOrQueueComponent; import org.sterl.spring.persistent_tasks.scheduler.component.TaskExecutorComponent; import org.sterl.spring.persistent_tasks.scheduler.entity.SchedulerEntity; import org.sterl.spring.persistent_tasks.trigger.TriggerService; import org.sterl.spring.persistent_tasks.trigger.event.TriggerAddedEvent; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; @@ -150,7 +150,7 @@ public List> triggerNextTasks(OffsetDateTime timeDue) { * available it is resolved */ @Transactional(timeout = 10) - public TriggerKey runOrQueue(AddTriggerRequest triggerRequest) { + public TriggerKey runOrQueue(TriggerRequest triggerRequest) { return runOrQueue.execute(triggerRequest); } @@ -166,16 +166,16 @@ public SchedulerEntity getStatus() { } @Transactional - public List rescheduleAbandonedTasks(OffsetDateTime timeout) { + public List rescheduleAbandonedTriggers(OffsetDateTime timeout) { var schedulers = editSchedulerStatus.findOnlineSchedulers(timeout); - final List runningKeys = this.taskExecutor.getRunningTriggers().stream().map(TriggerEntity::getKey) + final List runningKeys = this.taskExecutor.getRunningTriggers().stream().map(RunningTriggerEntity::getKey) .toList(); int running = triggerService.markTriggersAsRunning(runningKeys, name); log.atLevel(running > 0 ? Level.INFO : Level.DEBUG).log("({}) - {} trigger(s) are running on {} schedulers", running, runningKeys, schedulers); - return triggerService.rescheduleAbandonedTasks(timeout); + return triggerService.rescheduleAbandoned(timeout); } public List listAll() { @@ -184,7 +184,7 @@ public List listAll() { public Collection> getRunning() { return taskExecutor.getRunningTasks(); } - public List getRunningTriggers() { + public List getRunningTriggers() { return taskExecutor.getRunningTriggers(); } public boolean hasRunningTriggers() { 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 51d6d77f2..c6bee8b61 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 @@ -9,21 +9,23 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.sterl.spring.persistent_tasks.scheduler.config.ConditionalSchedulerServiceByProperty; +import org.sterl.spring.persistent_tasks.shared.TimersEnabled; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +@TimersEnabled @ConditionalSchedulerServiceByProperty @Service @RequiredArgsConstructor @Slf4j class SchedulerTimer { - @Value("${spring.persistent-tasks.task-timeout:PT5M}") + @Value("${spring.persistent-tasks.trigger-timeout:PT5M}") private Duration taskTimeout = Duration.ofMinutes(5); private final Collection schedulerServices; - @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-rate:30}", timeUnit = TimeUnit.SECONDS) + @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-rate:60}", timeUnit = TimeUnit.SECONDS) void triggerNextTasks() { for (SchedulerService s : schedulerServices) { try { @@ -35,12 +37,12 @@ void triggerNextTasks() { } } - @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-persistentTask-timeout:300}", timeUnit = TimeUnit.SECONDS) + @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-abandoned-triggers:300}", timeUnit = TimeUnit.SECONDS) void rescheduleAbandonedTasks() { var timeout = OffsetDateTime.now().minus(taskTimeout); for (SchedulerService s : schedulerServices) { try { - final var count = s.rescheduleAbandonedTasks(timeout); + final var count = s.rescheduleAbandonedTriggers(timeout); log.debug("Found {} abandoned tasks for {}. Timeout={}", count.size(), s.getName(), timeout); } catch (Exception e) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/RunOrQueueComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/RunOrQueueComponent.java index fba03354b..2b29a8c61 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/RunOrQueueComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/RunOrQueueComponent.java @@ -6,10 +6,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,7 +25,7 @@ public class RunOrQueueComponent { private final String schedulerName; private final TriggerService triggerService; private final TaskExecutorComponent taskExecutor; - private final Map shouldRun = new ConcurrentHashMap<>(); + private final Map shouldRun = new ConcurrentHashMap<>(); /** * Runs the given trigger if a free threads are available and the runAt time is @@ -34,7 +34,7 @@ public class RunOrQueueComponent { * @return the reference to the {@link Future} with the key, if no threads are * available it is resolved */ - public TriggerKey execute(AddTriggerRequest triggerRequest) { + public TriggerKey execute(TriggerRequest triggerRequest) { var trigger = triggerService.queue(triggerRequest); trigger = offerToRun(trigger); @@ -42,7 +42,7 @@ public TriggerKey execute(AddTriggerRequest triggerR return trigger.getKey(); } - private TriggerEntity offerToRun(TriggerEntity trigger) { + private RunningTriggerEntity offerToRun(RunningTriggerEntity trigger) { if (!trigger.shouldRunInFuture()) { if (taskExecutor.getFreeThreads() > 0) { trigger = triggerService.markTriggersAsRunning(trigger, schedulerName); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/TaskExecutorComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/TaskExecutorComponent.java index bd1ee694d..4b6696d93 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/TaskExecutorComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/component/TaskExecutorComponent.java @@ -20,7 +20,7 @@ import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.scheduler.config.SchedulerThreadFactory; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.Getter; import lombok.Setter; @@ -44,7 +44,7 @@ public class TaskExecutorComponent { private Duration maxShutdownWaitTime = Duration.ofSeconds(10); @Nullable private ExecutorService executor; - private final ConcurrentHashMap> runningTasks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> runningTasks = new ConcurrentHashMap<>(); private final AtomicBoolean stopped = new AtomicBoolean(true); private final Lock lock = new ReentrantLock(true); @@ -58,19 +58,19 @@ public TaskExecutorComponent(String schedulerName, TriggerService triggerService } @NonNull - public List> submit(List trigger) { + public List> submit(List trigger) { if (trigger == null || trigger.isEmpty()) return Collections.emptyList(); final List> result = new ArrayList<>(trigger.size()); - for (TriggerEntity triggerEntity : trigger) { + for (RunningTriggerEntity triggerEntity : trigger) { result.add(submit(triggerEntity)); } return result; } @NonNull - public Future submit(@Nullable TriggerEntity trigger) { + public Future submit(@Nullable RunningTriggerEntity trigger) { if (trigger == null) { return CompletableFuture.completedFuture(null); } @@ -95,7 +95,7 @@ private void assertStarted() { } } - private TriggerKey runTrigger(TriggerEntity trigger) { + private TriggerKey runTrigger(RunningTriggerEntity trigger) { try { triggerService.run(trigger); return trigger.getKey(); @@ -103,7 +103,7 @@ private TriggerKey runTrigger(TriggerEntity trigger) { lock.lock(); try { if (runningTasks.remove(trigger) == null && runningTasks.size() > 0) { - var runningKeys = runningTasks.keySet().stream().map(TriggerEntity::key).toList(); + var runningKeys = runningTasks.keySet().stream().map(RunningTriggerEntity::key).toList(); log.error("Failed to remove trigger with {} - {}", trigger.key(), runningKeys); } } finally { @@ -186,7 +186,7 @@ public Collection> getRunningTasks() { return runningTasks.values(); } - public List getRunningTriggers() { + public List getRunningTriggers() { var doneAndNotRemovedFutures = this.runningTasks.entrySet().stream().filter(e -> e.getValue().isDone()) .toList(); @@ -213,7 +213,7 @@ public int getMaxThreads() { return this.maxThreads.get(); } - public boolean isRunning(TriggerEntity trigger) { + public boolean isRunning(RunningTriggerEntity trigger) { return runningTasks.containsKey(trigger); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerConfig.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerConfig.java index 8ac92a71f..e2268d9e2 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerConfig.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerConfig.java @@ -77,6 +77,7 @@ SchedulerService schedulerService( maxShutdownWaitTime, trx); } + @Deprecated public static SchedulerService newSchedulerService( String name, MeterRegistry meterRegistry, diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerThreadFactory.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerThreadFactory.java index f1ef3e63a..2d2d7d62d 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerThreadFactory.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/config/SchedulerThreadFactory.java @@ -23,6 +23,6 @@ enum Type { }; SchedulerThreadFactory VIRTUAL_THREAD_POOL_FACTORY = (maxThreads) -> { - return Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("vpt-", 0) .factory()); + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("v-spt-", 0) .factory()); }; } 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 new file mode 100644 index 000000000..d69adb7dd --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/QueryHelper.java @@ -0,0 +1,26 @@ +package org.sterl.spring.persistent_tasks.shared; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.core.types.dsl.StringPath; + +public class QueryHelper { + + @Nullable + public static Predicate eq(@NonNull SimpleExpression path, @Nullable T value) { + if (value == null) return null; + return path.eq(value); + } + @Nullable + public static Predicate eqOrLike(@NonNull StringPath path, @Nullable String value) { + if (value == null) return null; + if (StringHelper.isSqlSearch(value)) { + return path.like(StringHelper.applySearchWildCard(value)); + } else { + return path.eq(value); + } + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/StringHelper.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/StringHelper.java index 81ed70c21..7bd1c8d20 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/StringHelper.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/StringHelper.java @@ -1,22 +1,31 @@ package org.sterl.spring.persistent_tasks.shared; -import org.sterl.spring.persistent_tasks.api.TriggerKey; - public class StringHelper { /** - * Replaces all * with % as needed + * Replaces all * with %, and [ with _ as needed. + *

+ * first call {@link #isSqlSearch(String)} + *

*/ public static String applySearchWildCard(String value) { if (value == null || value.length() == 0) return null; return value.replace('*', '%').replace('[', '_'); } - + /** - * Replaces all * with % as needed for the id. + * Checks if we have a wild card search + * + * first call this method, than + * @see #applySearchWildCard(String) + * */ - public static String applySearchWildCard(TriggerKey key) { - if (key == null) return null; - return applySearchWildCard(key.getId()); + public static boolean isSqlSearch(String value) { + if (value == null) return false; + return value.indexOf('%') > -1 || value.indexOf('*') > -1; + } + + public static boolean hasValue(String search) { + return search != null && search.length() > 0; } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/TimersEnabled.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/TimersEnabled.java new file mode 100644 index 000000000..2610603d1 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/TimersEnabled.java @@ -0,0 +1,17 @@ +package org.sterl.spring.persistent_tasks.shared; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@ConditionalOnProperty(name = "spring.persistent-tasks.timers-enabled", havingValue = "true", matchIfMissing = true) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +public @interface TimersEnabled { + +} 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 d2b2b86d4..409fcf3e7 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 @@ -3,21 +3,22 @@ import org.springframework.lang.NonNull; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer; -public enum ToTrigger implements ExtendetConvert { +public enum ToTrigger implements ExtendetConvert { INSTANCE; private final static StateSerializer SERIALIZER = new StateSerializer(); @NonNull @Override - public Trigger convert(@NonNull HasTriggerData hasData) { + public Trigger convert(@NonNull HasTrigger hasData) { final var source = hasData.getData(); final var result = new Trigger(); result.setKey(source.getKey()); result.setCorrelationId(source.getCorrelationId()); + result.setTag(source.getTag()); result.setCreatedTime(source.getCreatedTime()); result.setEnd(source.getEnd()); result.setExceptionName(source.getExceptionName()); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTriggerData.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java similarity index 94% rename from core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTriggerData.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java index 6f0313203..be3a10a2a 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTriggerData.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/HasTrigger.java @@ -6,8 +6,8 @@ import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -public interface HasTriggerData { - TriggerData getData(); +public interface HasTrigger { + TriggerEntity getData(); default TriggerKey key() { return getData().getKey(); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerData.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerEntity.java similarity index 89% rename from core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerData.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerEntity.java index 21ad8fbfa..fe0559cd9 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerData.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerEntity.java @@ -19,6 +19,7 @@ import lombok.Builder; import lombok.Builder.Default; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; @@ -29,9 +30,10 @@ @NoArgsConstructor @AllArgsConstructor @Embeddable -@ToString(of = {"key", "correlationId", "status", "priority", "executionCount", "createdTime", "runAt", "start", "end"}) +@ToString(of = {"key", "correlationId", "tag", "status", "priority", "executionCount", "createdTime", "runAt", "start", "end"}) +@EqualsAndHashCode(of = "key") @Builder(toBuilder = true) -public class TriggerData { +public class TriggerEntity { public void updateRunningDuration() { if (start != null && end != null) { @@ -48,6 +50,8 @@ public void updateRunningDuration() { ) ) private TriggerKey key; + + private String tag; @Column(nullable = true, updatable = false) private String correlationId; @@ -91,7 +95,7 @@ public void updateRunningDuration() { @Lob private String lastException; - public TriggerData copy() { + public TriggerEntity copy() { return this.toBuilder().build(); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerDataRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerDataRepository.java deleted file mode 100644 index 34f20a58d..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerDataRepository.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.sterl.spring.persistent_tasks.shared.repository; - -import java.time.OffsetDateTime; -import java.util.List; -import java.util.Set; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.NoRepositoryBean; -import org.springframework.data.repository.query.Param; -import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; - -@NoRepositoryBean -public interface TriggerDataRepository extends JpaRepository { - Sort DEFAULT_SORT = Sort.by(Direction.ASC, "data.createdTime"); - - default Pageable applyDefaultSortIfNeeded(Pageable page) { - var result = page; - if (page.getSort() == Sort.unsorted()) { - result = PageRequest.of(page.getPageNumber(), page.getPageSize(), DEFAULT_SORT); - } - return result; - } - - @Query(""" - SELECT e FROM #{#entityName} e - WHERE ((:id IS NULL OR e.data.key.id LIKE :id) - OR (:id IS NULL OR e.data.correlationId LIKE :id)) - AND (:taskName IS NULL OR e.data.key.taskName = :taskName) - AND (:status IS NULL OR e.data.status = :status) - """) - Page findAll(@Param("id") String id, - @Param("taskName") String taskName, - @Param("status") TriggerStatus status, - Pageable page); - - @Query(""" - SELECT e FROM #{#entityName} e - WHERE e.data.key.taskName = :taskName - """) - Page findAll(@Param("taskName") String taskName, Pageable page); - - @Query(""" - SELECT COUNT(e.data.key) - FROM #{#entityName} e WHERE e.data.key.taskName = :taskName - """) - long countByTaskName(@Param("taskName") String taskName); - - @Query(""" - SELECT COUNT(e.data.key) - FROM #{#entityName} e WHERE e.data.key = :key - """) - long countByKey(@Param("key") TriggerKey key); - - @Query(""" - SELECT COUNT(e.id) - FROM #{#entityName} e - WHERE e.data.status = :status - """) - long countByStatus(@Param("status") TriggerStatus status); - - @Query(""" - SELECT COUNT(e.id) - FROM #{#entityName} e - WHERE e.data.status IN ( :status ) - """) - long countByStatus(@Param("status") Set status); - - @Query(""" - DELETE FROM #{#entityName} e - WHERE e.data.createdTime < :age - """) - @Modifying - long deleteOlderThan(@Param("age") OffsetDateTime age); - - @Query(""" - SELECT e FROM #{#entityName} e - WHERE e.data.correlationId = :correlationId - """) - List findByCorrelationId(@Param("correlationId") String correlationId, Pageable page); -} 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 new file mode 100644 index 000000000..edee7f7c6 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/repository/TriggerRepository.java @@ -0,0 +1,102 @@ +package org.sterl.spring.persistent_tasks.shared.repository; + +import java.time.OffsetDateTime; +import java.util.Set; + +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.NoRepositoryBean; +import org.springframework.data.repository.query.Param; +import org.springframework.lang.NonNull; +import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerSearch; +import org.sterl.spring.persistent_tasks.api.TriggerStatus; +import org.sterl.spring.persistent_tasks.shared.QueryHelper; +import org.sterl.spring.persistent_tasks.shared.StringHelper; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.QTriggerEntity; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Predicate; + +@NoRepositoryBean +public interface TriggerRepository extends JpaRepository, QuerydslPredicateExecutor { + + default Predicate buildSearch( + @NonNull QTriggerEntity qData, + @NonNull TriggerSearch search) { + + final var predicate = new BooleanBuilder(); + + if (search.getSearch() != null) { + final var value = StringHelper.applySearchWildCard(search.getSearch()); + Predicate pId; + if (StringHelper.isSqlSearch(value)) { + pId = ExpressionUtils.anyOf( + qData.key.id.like(value), + qData.correlationId.like(value), + qData.tag.like(value) + ); + } else { + pId = ExpressionUtils.anyOf( + qData.key.id.eq(value), + qData.correlationId.eq(value), + qData.tag.eq(value) + ); + } + predicate.and(pId); + } + + predicate.and(QueryHelper.eqOrLike(qData.key.id, search.getKeyId())); + predicate.and(QueryHelper.eqOrLike(qData.key.taskName, search.getTaskName())); + predicate.and(QueryHelper.eqOrLike(qData.correlationId, search.getCorrelationId())); + predicate.and(QueryHelper.eqOrLike(qData.tag, search.getTag())); + predicate.and(QueryHelper.eq(qData.status, search.getStatus())); + + return predicate; + } + + @Query(""" + SELECT e FROM #{#entityName} e + WHERE e.data.key.taskName = :taskName + """) + Page findAll(@Param("taskName") String taskName, Pageable page); + + @Query(""" + SELECT COUNT(e.data.key) + FROM #{#entityName} e WHERE e.data.key.taskName = :taskName + """) + long countByTaskName(@Param("taskName") String taskName); + + @Query(""" + SELECT COUNT(e.data.key) + FROM #{#entityName} e WHERE e.data.key = :key + """) + long countByKey(@Param("key") TriggerKey key); + + @Query(""" + SELECT COUNT(e.id) + FROM #{#entityName} e + WHERE e.data.status = :status + """) + long countByStatus(@Param("status") TriggerStatus status); + + @Query(""" + SELECT COUNT(e.id) + FROM #{#entityName} e + WHERE e.data.status IN ( :status ) + """) + long countByStatus(@Param("status") Set status); + + @Query(""" + DELETE FROM #{#entityName} e + WHERE e.data.createdTime < :age + """) + @Modifying + long deleteOlderThan(@Param("age") OffsetDateTime age); +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/TaskService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/TaskService.java index 1c26941f0..5c694b383 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/task/TaskService.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/TaskService.java @@ -1,8 +1,10 @@ package org.sterl.spring.persistent_tasks.task; import java.io.Serializable; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import org.springframework.lang.NonNull; @@ -22,6 +24,7 @@ public class TaskService { private final TaskTransactionComponent taskTransactionComponent; private final TaskRepository taskRepository; + private final Map, Optional> cache = new ConcurrentHashMap<>(); public Set> findAllTaskIds() { return this.taskRepository.all(); @@ -31,9 +34,14 @@ public Optional> get(TaskId id) { return taskRepository.get(id); } - public Optional getTransactionTemplate( + /** + * Returns a {@link TransactionTemplate} if the task and the framework may join transaction. + */ + public Optional getTransactionTemplateIfJoinable( PersistentTask task) { - return taskTransactionComponent.getTransactionTemplate(task); + + return cache.computeIfAbsent(task, + t -> taskTransactionComponent.buildOrGetDefaultTransactionTemplate(t)); } /** @@ -59,7 +67,6 @@ public PersistentTask assertIsKnown(@NonNull TaskId< */ public TaskId register(String name, Consumer task) { return register(name, new PersistentTask() { - private static final long serialVersionUID = 1L; @Override public void accept(@Nullable Serializable state) { task.accept(state); @@ -78,7 +85,7 @@ public TaskId register(String name, PersistentTask TaskId register(TaskId id, PersistentTask task) { - taskTransactionComponent.getTransactionTemplate(task); + taskTransactionComponent.buildOrGetDefaultTransactionTemplate(task); return taskRepository.addTask(id, task); } /** diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java index adf29edd8..aae64a319 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java @@ -2,10 +2,8 @@ import java.io.Serializable; import java.util.EnumSet; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; @@ -28,22 +26,28 @@ public class TaskTransactionComponent { private final TransactionTemplate template; private final Set joinTransaction = EnumSet.of( Propagation.MANDATORY, Propagation.REQUIRED, Propagation.SUPPORTS); - private final Map, Optional> cache = new ConcurrentHashMap<>(); - public Optional getTransactionTemplate(PersistentTask task) { - if (cache.containsKey(task)) return cache.get(task); + /** + * Returns a transaction template if and only if we can join the transaction with the anotated apply method + */ + public Optional buildOrGetDefaultTransactionTemplate(PersistentTask task) { + Optional result; + var annotation = ReflectionUtil.getAnnotation(task, Transactional.class); + if (annotation == null) { + result = useDefaultTransactionTemplate(task); + } else { + result = Optional.ofNullable(builTransactionTemplate(task, annotation)); + } + return result; + } + public Optional useDefaultTransactionTemplate(PersistentTask task) { Optional result; // first we apply a default if (task.isTransactional()) result = Optional.of(template); else result = Optional.empty(); - var annotation = ReflectionUtil.getAnnotation(task, Transactional.class); - if (annotation != null) { - log.debug("found {} on task={}, creating custom ", annotation, task.getClass().getName()); - result = Optional.ofNullable(builTransactionTemplate(task, annotation)); - } - cache.put(task, result); + log.debug("Using default template={} for task={}", result, task.getClass().getName()); return result; } @@ -58,10 +62,10 @@ private TransactionTemplate builTransactionTemplate(PersistentTask run(@Nullable TriggerEntity trigger) { + public Optional run(@Nullable RunningTriggerEntity trigger) { return runTrigger.execute(trigger); } @@ -58,11 +61,11 @@ public Optional run(@Nullable TriggerEntity trigger) { * * @param triggerKey the key to trigger which should be executed * @param runningOn just any string, could be test for testing, usually the scheduler name - * @return the reference to the found an executed {@link TriggerEntity} + * @return the reference to the found an executed {@link RunningTriggerEntity} */ @Transactional(propagation = Propagation.NEVER) - public Optional run(TriggerKey triggerKey, String runningOn) { - final TriggerEntity trigger = lockNextTrigger.lock(triggerKey, runningOn); + public Optional run(TriggerKey triggerKey, String runningOn) { + final RunningTriggerEntity trigger = lockNextTrigger.lock(triggerKey, runningOn); if (trigger == null) { return Optional.empty(); } @@ -70,13 +73,13 @@ public Optional run(TriggerKey triggerKey, String runningOn) { } @Transactional(propagation = Propagation.NEVER) - public Optional run(@Nullable AddTriggerRequest request, String runningOn) { + public Optional run(@Nullable TriggerRequest request, String runningOn) { var trigger = queue(request); trigger = lockNextTrigger.lock(trigger.getKey(), runningOn); return run(trigger); } - public TriggerEntity markTriggersAsRunning(TriggerEntity trigger, String runOn) { + public RunningTriggerEntity markTriggersAsRunning(RunningTriggerEntity trigger, String runOn) { return trigger.runOn(runOn); } @@ -84,27 +87,26 @@ public int markTriggersAsRunning(Collection keys, String runOn) { return this.editTrigger.markTriggersAsRunning(keys, runOn); } - public TriggerEntity lockNextTrigger(String runOn) { - final List r = lockNextTrigger.loadNext(runOn, 1, OffsetDateTime.now()); + public RunningTriggerEntity lockNextTrigger(String runOn) { + final List r = lockNextTrigger.loadNext(runOn, 1, OffsetDateTime.now()); return r.isEmpty() ? null : r.get(0); } - public List lockNextTrigger(String runOn, int count, OffsetDateTime timeDueAt) { + public List lockNextTrigger(String runOn, int count, OffsetDateTime timeDueAt) { return lockNextTrigger.loadNext(runOn, count, timeDueAt); } - public Optional get(TriggerKey triggerKey) { + public Optional get(TriggerKey triggerKey) { return readTrigger.get(triggerKey); } @Transactional(readOnly = true , timeout = 10) - public Page findAllTriggers( - @Nullable TriggerKey key, @Nullable TriggerStatus status, Pageable page) { - return this.readTrigger.listTriggers(key, status, page); + public Page searchTriggers(@Nullable TriggerSearch search, Pageable page) { + return this.readTrigger.searchTriggers(search, page); } @Transactional(readOnly = true , timeout = 10) - public Page findAllTriggers(TaskId task, Pageable page) { + public Page findAllTriggers(TaskId task, Pageable page) { return this.readTrigger.listTriggers(task, page); } @@ -124,23 +126,36 @@ public boolean hasPendingTriggers() { * Adds or updates an existing trigger based on its {@link TriggerKey} * * @param the state type - * @param tigger the {@link AddTriggerRequest} to save - * @return the saved {@link TriggerEntity} + * @param tigger the {@link TriggerRequest} to save + * @return the saved {@link RunningTriggerEntity} * @throws IllegalStateException if the trigger already exists and is {@link TriggerStatus#RUNNING} */ - public TriggerEntity queue(AddTriggerRequest tigger) { + public RunningTriggerEntity queue(TriggerRequest tigger) { taskService.assertIsKnown(tigger.taskId()); return editTrigger.addTrigger(tigger); } + + /** + * Will resume any found + * @param trigger + * @return + */ + public Page resume(TriggerRequest trigger) { + if (trigger.key().getId() == null && trigger.correlationId() == null) { + throw new IllegalArgumentException("Trigger ID or correlationId required to resume: " + trigger); + } + taskService.assertIsKnown(trigger.taskId()); + return editTrigger.resume(trigger); + } /** * If you changed your mind, cancel the persistentTask */ - public Optional cancel(TriggerKey key) { + public Optional cancel(TriggerKey key) { return editTrigger.cancelTask(key, null); } - public List cancel(Collection key) { + public List cancel(Collection key) { return key.stream().map(t -> editTrigger.cancelTask(t, null)) .filter(Optional::isPresent) .map(Optional::get) @@ -170,8 +185,8 @@ public long countTriggers(@Nullable TriggerStatus status) { * * Retry will be triggered based on the set strategy. */ - public List rescheduleAbandonedTasks(OffsetDateTime timeout) { - final List result = readTrigger.findTriggersLastPingAfter( + public List rescheduleAbandoned(OffsetDateTime timeout) { + final List result = readTrigger.findTriggersLastPingAfter( timeout); final var e = new IllegalStateException("Trigger abandoned - timeout: " + timeout); result.forEach(t -> { @@ -182,14 +197,21 @@ public List rescheduleAbandonedTasks(OffsetDateTime timeout) { log.debug("rescheduled {} triggers", result.size()); return result; } + + public List expireTimeoutTriggers() { + return readTrigger.findTriggersTimeoutOut(20) + .stream() + .map(editTrigger::expireTrigger) + .toList(); + } public long countTriggers() { return readTrigger.countByStatus(null); } - public Optional updateRunAt(TriggerKey key, OffsetDateTime time) { + public Optional updateRunAt(TriggerKey key, OffsetDateTime time) { return readTrigger.get(key).map(t -> { - if (t.getData().getStatus() != TriggerStatus.WAITING) { + if (t.getData().getStatus() == TriggerStatus.RUNNING) { throw new IllegalStateException("Cannot update status of " + key + " as the current status is: " + t.getData().getStatus()); } @@ -197,9 +219,4 @@ public Optional updateRunAt(TriggerKey key, OffsetDateTime time) return t; }); } - - public List findTriggerByCorrelationId(String correlationId, Pageable page) { - return readTrigger.findTriggerByCorrelationId(correlationId, page); - - } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerTimer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerTimer.java new file mode 100644 index 000000000..b9a71e1f0 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerTimer.java @@ -0,0 +1,26 @@ +package org.sterl.spring.persistent_tasks.trigger; + +import java.util.concurrent.TimeUnit; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.shared.TimersEnabled; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@TimersEnabled +@Component +@RequiredArgsConstructor +@Slf4j +public class TriggerTimer { + + private final TriggerService triggerService; + + @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-awaiting-trigger-timeout:300}", timeUnit = TimeUnit.SECONDS) + void checkAwaitingTriggersForTimeout() { + int expired = triggerService.expireTimeoutTriggers().size(); + if (expired > 0) log.info("{} triggers have not received the signal, wait expired!", expired); + else log.debug("No expired triggers found."); + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java index 9c60a2751..3848f2e3b 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java @@ -1,17 +1,18 @@ package org.sterl.spring.persistent_tasks.trigger.api; +import org.springframework.lang.NonNull; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; public class TriggerConverter { - public enum FromTriggerEntity implements ExtendetConvert { + public enum FromTriggerEntity implements ExtendetConvert { INSTANCE; @Override - public Trigger convert(TriggerEntity source) { + public Trigger convert(@NonNull RunningTriggerEntity source) { var result = ToTrigger.INSTANCE.convert(source); result.setId(source.getId()); result.setInstanceId(source.getId()); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java index 8e0badb33..0c11a337c 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java @@ -3,7 +3,6 @@ import java.time.OffsetDateTime; import java.util.Optional; -import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.PageableDefault; @@ -14,11 +13,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.TriggerKey; -import org.sterl.spring.persistent_tasks.api.TriggerStatus; +import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.trigger.TriggerService; import org.sterl.spring.persistent_tasks.trigger.api.TriggerConverter.FromTriggerEntity; @@ -38,14 +36,11 @@ public long count() { @GetMapping("triggers") public PagedModel list( - @RequestParam(name = "id", required = false) String id, - @RequestParam(name = "taskName", required = false) String taskName, - @RequestParam(name = "status", required = false) TriggerStatus status, + TriggerSearch search, @PageableDefault(size = 100, direction = Direction.ASC, sort = "data.runAt") Pageable pageable) { - var key = new TriggerKey(StringUtils.trimToNull(id), StringUtils.trimToNull(taskName)); return FromTriggerEntity.INSTANCE.toPage( - triggerService.findAllTriggers(key, status, pageable)); + triggerService.searchTriggers(search, pageable)); } @PostMapping("triggers/{taskName}/{id}/run-at") diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/EditTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/EditTriggerComponent.java index b754b5345..48b6596d4 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/EditTriggerComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/EditTriggerComponent.java @@ -7,22 +7,27 @@ import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; +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.api.task.RunningTriggerContextHolder; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; 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.TriggerRunningEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.repository.RunningTriggerRepository; + +import com.github.f4b6a3.uuid.UuidCreator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,10 +40,12 @@ public class EditTriggerComponent { private final ApplicationEventPublisher publisher; private final StateSerializer stateSerializer = new StateSerializer(); - private final TriggerRepository triggerRepository; + private final ToTriggerData toTriggerData = new ToTriggerData(stateSerializer); + private final ReadTriggerComponent readTrigger; + private final RunningTriggerRepository triggerRepository; - public Optional completeTaskWithSuccess(TriggerKey key, Serializable state) { - final Optional result = triggerRepository.findByKey(key); + public Optional completeTaskWithSuccess(TriggerKey key, Serializable state) { + final Optional result = readTrigger.get(key); result.ifPresent(t -> { t.complete(null); @@ -53,12 +60,12 @@ public Optional completeTaskWithSuccess(TriggerKey key, Serializa /** * Sets error based on the fact if an exception is given or not. */ - public Optional failTrigger( + public Optional failTrigger( TriggerKey key, Serializable state, Exception e, OffsetDateTime retryAt) { - final Optional result = triggerRepository.findByKey(key); + final Optional result = triggerRepository.findByKey(key); result.ifPresent(t -> { @@ -80,24 +87,34 @@ public Optional failTrigger( return result; } - public Optional cancelTask(TriggerKey id, Exception e) { + public Optional cancelTask(TriggerKey id, Exception e) { return triggerRepository // .findByKey(id) // - .map(t -> cancelTask(t, e)); + .map(t -> cancelTrigger(t, e)); } - private TriggerEntity cancelTask(TriggerEntity t, Exception e) { + private RunningTriggerEntity cancelTrigger(RunningTriggerEntity t, Exception e) { t.cancel(e); + publisher.publishEvent(new TriggerCanceledEvent( - t.getId(), t.copyData(), + t.getId(), + t.copyData(), stateSerializer.deserializeOrNull(t.getData().getState()))); + triggerRepository.delete(t); return t; } - public TriggerEntity addTrigger(AddTriggerRequest tigger) { + public RunningTriggerEntity addTrigger(TriggerRequest tigger) { var result = toTriggerEntity(tigger); - final Optional existing = triggerRepository.findByKey(result.getKey()); + final Optional existing; + + if (result.key().getId() == null) { + existing = Optional.empty(); + result.getKey().setId(UuidCreator.getTimeOrderedEpochFast().toString()); + } + else existing = triggerRepository.findByKey(result.getKey()); + if (existing.isPresent()) { if (existing.get().isRunning()) throw new IllegalStateException("Cannot update a running trigger " + result.getKey()); @@ -109,33 +126,56 @@ public TriggerEntity addTrigger(AddTriggerRequest ti result = triggerRepository.save(result); log.debug("Added trigger={}", result); } + publisher.publishEvent(new TriggerAddedEvent( result.getId(), result.copyData(), tigger.state())); + return result; } + + public Page resume(TriggerRequest trigger) { + var search = TriggerSearch.forTriggerRequest(trigger); + search.setStatus(TriggerStatus.AWAITING_SIGNAL); + + var foundTriggers = readTrigger.searchTriggers(search, Pageable.ofSize(100)); + + log.debug("Resuming {} triggers for given data {}", foundTriggers.getSize(), trigger); + foundTriggers.forEach(t -> { + var newData = toTriggerEntity(trigger); + newData.getData().setKey(t.getKey()); + newData.getData().setCorrelationId(t.getData().getCorrelationId()); + + t.setData(newData.getData()); + t.runAt(trigger.runtAt()); + + publisher.publishEvent(new TriggerResumedEvent(t.getId(), t.copyData(), trigger.state())); + }); + return foundTriggers; + } + + public RunningTriggerEntity expireTrigger(RunningTriggerEntity t) { + t.getData().setStatus(TriggerStatus.EXPIRED_SIGNAL); + t.getData().setStart(null); + t.getData().setEnd(null); + t.getData().updateRunningDuration(); + + publisher.publishEvent(new TriggerExpiredEvent( + t.getId(), t.copyData(), + stateSerializer.deserializeOrNull(t.getData().getState()))); + return t; + } @NonNull - public List addTriggers(Collection> newTriggers) { + public List addTriggers(Collection> newTriggers) { return newTriggers.stream() .map(this::addTrigger) .toList(); } - private TriggerEntity toTriggerEntity(AddTriggerRequest trigger) { - byte[] state = stateSerializer.serialize(trigger.state()); - - var correlationId = RunningTriggerContextHolder.buildOrGetCorrelationId(trigger.correlationId()); - final var data = TriggerData.builder() - .key(trigger.key()) - .runAt(trigger.runtAt()) - .priority(trigger.priority()) - .state(state) - .correlationId(correlationId); - - final var t = TriggerEntity.builder() - .data(data.build()) + private RunningTriggerEntity toTriggerEntity(TriggerRequest trigger) { + return RunningTriggerEntity.builder() + .data(toTriggerData.convert(trigger)) .build(); - return t; } public void deleteAll() { @@ -143,7 +183,7 @@ public void deleteAll() { this.triggerRepository.deleteAllInBatch(); } - public void deleteTrigger(TriggerEntity trigger) { + public void deleteTrigger(RunningTriggerEntity trigger) { this.triggerRepository.delete(trigger); } @@ -153,7 +193,7 @@ public int markTriggersAsRunning(Collection keys, String runOn) { } @Transactional(propagation = Propagation.SUPPORTS) - public void triggerIsNowRunning(TriggerEntity trigger, Serializable state) { + public void triggerIsNowRunning(RunningTriggerEntity trigger, Serializable state) { if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn()); publisher.publishEvent(new TriggerRunningEvent( trigger.getId(), trigger.copyData(), state, trigger.getRunningOn())); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/FailTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/FailTriggerComponent.java index 4bacbcb7f..08c3146b9 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/FailTriggerComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/FailTriggerComponent.java @@ -10,7 +10,7 @@ import org.sterl.spring.persistent_tasks.task.exception.CancelTaskException; import org.sterl.spring.persistent_tasks.task.exception.FailTaskNoRetryException; import org.sterl.spring.persistent_tasks.trigger.model.RunTaskWithStateCommand; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,7 +22,7 @@ public class FailTriggerComponent { private final EditTriggerComponent editTrigger; - public Optional execute(RunTaskWithStateCommand runTaskWithStateCommand, Exception e) { + public Optional execute(RunTaskWithStateCommand runTaskWithStateCommand, Exception e) { var trigger = runTaskWithStateCommand.trigger(); var task = runTaskWithStateCommand.task(); @@ -33,16 +33,16 @@ public Optional execute(RunTaskWithStateCommand runTaskWithStateC /** * Fails the given trigger, no retry will be applied! */ - public Optional execute(TriggerEntity trigger, Exception e) { + public Optional execute(RunningTriggerEntity trigger, Exception e) { return execute(null, trigger, null, e); } - public Optional execute( + public Optional execute( @Nullable PersistentTask task, - TriggerEntity trigger, + RunningTriggerEntity trigger, @Nullable T state, Exception e) { - Optional result; + Optional result; if (e instanceof CancelTaskException) { log.info("Cancel of a running trigger={} requested", trigger.getKey()); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/LockNextTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/LockNextTriggerComponent.java index d1c3a3d17..48996f037 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/LockNextTriggerComponent.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/LockNextTriggerComponent.java @@ -8,8 +8,8 @@ import org.springframework.transaction.annotation.Transactional; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.repository.RunningTriggerRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,9 +23,9 @@ @RequiredArgsConstructor public class LockNextTriggerComponent { - private final TriggerRepository triggerRepository; + private final RunningTriggerRepository triggerRepository; - public List loadNext(String runningOn, int count, OffsetDateTime timeDueAt) { + public List loadNext(String runningOn, int count, OffsetDateTime timeDueAt) { final var tasks = triggerRepository.loadNextTasks( timeDueAt, TriggerStatus.WAITING, PageRequest.of(0, count)); @@ -35,8 +35,8 @@ public List loadNext(String runningOn, int count, OffsetDateTime return tasks; } - public TriggerEntity lock(TriggerKey id, String runningOn) { - final TriggerEntity result = triggerRepository.lockByKey(id); + public RunningTriggerEntity lock(TriggerKey id, String runningOn) { + final RunningTriggerEntity result = triggerRepository.lockByKey(id); if (result != null) { result.runOn(runningOn); } 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 d3e948d9d..5ed785c49 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 @@ -10,11 +10,12 @@ import org.springframework.lang.Nullable; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.shared.StringHelper; import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalCompontant; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.trigger.model.QRunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.repository.RunningTriggerRepository; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; @@ -22,7 +23,7 @@ @TransactionalCompontant @RequiredArgsConstructor public class ReadTriggerComponent { - private final TriggerRepository triggerRepository; + private final RunningTriggerRepository triggerRepository; public long countByTaskName(@NotNull String name) { return triggerRepository.countByTaskName(name); @@ -33,8 +34,8 @@ public long countByStatus(@Nullable TriggerStatus status) { return triggerRepository.countByStatus(status); } - public Optional get(TriggerKey id) { - return triggerRepository.findByKey(id); + public Optional get(TriggerKey key) { + return triggerRepository.findByKey(key); } /** @@ -47,24 +48,30 @@ public boolean hasPendingTriggers() { return false; } - public List findTriggersLastPingAfter(OffsetDateTime dateTime) { + public List findTriggersLastPingAfter(OffsetDateTime dateTime) { return triggerRepository.findTriggersLastPingAfter(dateTime); } - public Page listTriggers(@Nullable TriggerKey key, - @Nullable TriggerStatus status, Pageable page) { - if (key == null && status == null) return triggerRepository.findAll(page); - final var id = StringHelper.applySearchWildCard(key); - final var name = key == null ? null : key.getTaskName(); - return triggerRepository.findAll(id, name, status, page); + public Page searchTriggers(@Nullable TriggerSearch search, Pageable page) { + page = TriggerSearch.applyDefaultSortIfNeeded(page); + if (search != null && search.hasValue()) { + var p = triggerRepository.buildSearch(QRunningTriggerEntity.runningTriggerEntity.data, search); + return triggerRepository.findAll(p, page); + } else { + return triggerRepository.findAll(page); + } + } - public Page listTriggers(TaskId task, Pageable page) { + public Page listTriggers(TaskId task, Pageable page) { if (task == null) return triggerRepository.findAll(page); return triggerRepository.findAll(task.name(), page); } - - public List findTriggerByCorrelationId(String correlationId, Pageable page) { - return triggerRepository.findByCorrelationId(correlationId, triggerRepository.applyDefaultSortIfNeeded(page)); + + public List findTriggersTimeoutOut(int max) { + return triggerRepository.findByStatusAndRunAtAfter( + TriggerStatus.AWAITING_SIGNAL, + OffsetDateTime.now(), + Pageable.ofSize(max)); } } 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 8bdf0587f..068918a9e 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 @@ -9,7 +9,7 @@ import org.sterl.spring.persistent_tasks.api.task.RunningTriggerContextHolder; import org.sterl.spring.persistent_tasks.task.TaskService; import org.sterl.spring.persistent_tasks.trigger.model.RunTaskWithStateCommand; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,10 +25,10 @@ public class RunTriggerComponent { private final StateSerializer serializer = new StateSerializer(); /** - * Will execute the given {@link TriggerEntity} and handle any errors etc. + * Will execute the given {@link RunningTriggerEntity} and handle any errors etc. */ @Transactional(propagation = Propagation.NEVER) - public Optional execute(TriggerEntity trigger) { + public Optional execute(RunningTriggerEntity trigger) { if (trigger == null) { return Optional.empty(); } @@ -48,10 +48,10 @@ public Optional execute(TriggerEntity trigger) { } @Nullable - private RunTaskWithStateCommand buildTaskWithStateFor(TriggerEntity trigger) { + private RunTaskWithStateCommand buildTaskWithStateFor(RunningTriggerEntity trigger) { try { final var task = taskService.assertIsKnown(trigger.newTaskId()); - final var trx = taskService.getTransactionTemplate(task); + final var trx = taskService.getTransactionTemplateIfJoinable(task); final var state = serializer.deserialize(trigger.getData().getState()); return new RunTaskWithStateCommand(task, trx, state, trigger); } catch (Exception e) { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java new file mode 100644 index 000000000..d3f56ea08 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java @@ -0,0 +1,34 @@ +package org.sterl.spring.persistent_tasks.trigger.component; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; +import org.sterl.spring.persistent_tasks.api.TriggerStatus; +import org.sterl.spring.persistent_tasks.api.task.RunningTriggerContextHolder; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ToTriggerData implements Converter, TriggerEntity> { + + private final StateSerializer stateSerializer; + + @Override + @Nullable + public TriggerEntity convert(@NonNull TriggerRequest trigger) { + var correlationId = RunningTriggerContextHolder.buildOrGetCorrelationId(trigger.correlationId()); + byte[] state = stateSerializer.serialize(trigger.state()); + final var data = TriggerEntity.builder() + .key(trigger.key()) + .runAt(trigger.runtAt()) + .priority(trigger.priority()) + .state(state) + .status(trigger.status() == null ? TriggerStatus.WAITING : trigger.status()) + .correlationId(correlationId) + .tag(trigger.tag()); + + return data.build(); + } +} 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 2bcf9fc0b..057ad3163 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 @@ -2,7 +2,7 @@ import java.io.Serializable; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** * Fired if a new trigger is added. @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerAddedEvent(long id, TriggerData 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 cc0e048e5..79e24d506 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 @@ -2,7 +2,7 @@ import java.io.Serializable; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** * Fired if a trigger could be canceled before it is running. @@ -10,7 +10,7 @@ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerCanceledEvent(long id, TriggerData 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 new file mode 100644 index 000000000..431363ab3 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerExpiredEvent.java @@ -0,0 +1,19 @@ +package org.sterl.spring.persistent_tasks.trigger.event; + +import java.io.Serializable; + +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; + +/** + * Fired if a new trigger is added. + *

+ * 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 { + + @Override + public boolean isDone() { + return false; + } +} 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 25ee4c09d..dc72339b0 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 @@ -3,7 +3,7 @@ import java.io.Serializable; import java.time.OffsetDateTime; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** *

@@ -11,7 +11,7 @@ *

*/ public record TriggerFailedEvent(long id, - TriggerData data, Serializable state, + 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 89beddb99..4b5cdc1e3 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 @@ -2,23 +2,21 @@ import java.io.Serializable; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.sterl.spring.persistent_tasks.api.event.PersistentTasksEvent; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** * Tag any events which are fired in case something changes on a trigger. * The attached data is already a copy, any modification to this data will have no effect. */ -public interface TriggerLifeCycleEvent extends HasTriggerData, PersistentTasksEvent { - default TriggerData getData() { +public interface TriggerLifeCycleEvent extends HasTrigger, PersistentTasksEvent { + default TriggerEntity getData() { return data(); } long id(); - @NonNull - TriggerData data(); + TriggerEntity data(); @Nullable Serializable state(); /** 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 new file mode 100644 index 000000000..032f5d3b2 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerResumedEvent.java @@ -0,0 +1,19 @@ +package org.sterl.spring.persistent_tasks.trigger.event; + +import java.io.Serializable; + +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; + +/** + * Fired if a new trigger is added. + *

+ * 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 { + + @Override + public boolean isDone() { + return false; + } +} 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 3b7de39f3..e8f2924be 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 @@ -2,7 +2,7 @@ import java.io.Serializable; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** * Event fired before a trigger is executed @@ -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, TriggerData 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 fedb18b45..45690b68d 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 @@ -2,14 +2,14 @@ import java.io.Serializable; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; /** *

* Inside a transaction, it is save to join or listen for the AFTER_COMMIT *

*/ -public record TriggerSuccessEvent(long id, TriggerData 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/model/RunTaskWithStateCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java index a11c99387..4f9b08fce 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 @@ -7,22 +7,22 @@ import org.springframework.transaction.support.TransactionTemplate; import org.sterl.spring.persistent_tasks.api.task.PersistentTask; import org.sterl.spring.persistent_tasks.api.task.RunningTrigger; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +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.EditTriggerComponent; public record RunTaskWithStateCommand ( PersistentTask task, Optional trx, Serializable state, - TriggerEntity trigger, - RunningTrigger runningTrigger) implements HasTriggerData { + RunningTriggerEntity trigger, + RunningTrigger runningTrigger) implements HasTrigger { public RunTaskWithStateCommand( PersistentTask task, Optional trx, Serializable state, - TriggerEntity trigger) { + RunningTriggerEntity trigger) { this(task, trx, state, trigger, new RunningTrigger<>( @@ -33,7 +33,7 @@ public RunTaskWithStateCommand( )); } - public Optional execute(EditTriggerComponent editTrigger) { + public Optional execute(EditTriggerComponent editTrigger) { if (trx.isPresent()) { return trx.get().execute(t -> runTask(editTrigger)); } else { @@ -41,13 +41,12 @@ public Optional execute(EditTriggerComponent editTrigger) { } } - private Optional runTask(EditTriggerComponent editTrigger) { + private Optional runTask(EditTriggerComponent editTrigger) { editTrigger.triggerIsNowRunning(trigger, state); task.accept(state); var result = editTrigger.completeTaskWithSuccess(trigger.getKey(), state); - editTrigger.deleteTrigger(trigger); return result; } @@ -57,7 +56,7 @@ boolean hasValues(Collection elements) { } @Override - public TriggerData getData() { + public TriggerEntity getData() { return trigger.getData(); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/TriggerEntity.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java similarity index 60% rename from core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/TriggerEntity.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java index c29b94d9a..09e2f6dae 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/TriggerEntity.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunningTriggerEntity.java @@ -5,8 +5,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; import jakarta.annotation.Nullable; import jakarta.persistence.Column; @@ -25,20 +25,21 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "pt_task_triggers", indexes = { - @Index(name = "unq_pt_triggers_key", columnList = "trigger_id, task_name", unique = true), - @Index(name = "idx_pt_triggers_priority", columnList = "priority"), - @Index(name = "idx_pt_triggers_run_at", columnList = "run_at"), - @Index(name = "idx_pt_triggers_status", columnList = "status"), - @Index(name = "idx_pt_triggers_ping", columnList = "last_ping"), - @Index(name = "idx_pt_triggers_correlation_id", columnList = "correlation_id"), +@Table(name = "pt_running_triggers", indexes = { + @Index(name = "unq_pt_running_triggers_key", columnList = "trigger_id, task_name", unique = true), + @Index(name = "idx_pt_running_triggers_priority", columnList = "priority"), + @Index(name = "idx_pt_running_triggers_run_at", columnList = "run_at"), + @Index(name = "idx_pt_running_triggers_status", columnList = "status"), + @Index(name = "idx_pt_running_triggers_last_ping", columnList = "last_ping"), + @Index(name = "idx_pt_running_triggers_correlation_id", columnList = "correlation_id"), + @Index(name = "idx_pt_running_triggers_tag", columnList = "tag"), }) @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of = "id") -@Builder -public class TriggerEntity implements HasTriggerData { +@Builder(toBuilder = true) +public class RunningTriggerEntity implements HasTrigger { @GeneratedValue(generator = "seq_pt_task_triggers", strategy = GenerationType.SEQUENCE) @Column(updatable = false) @@ -47,7 +48,7 @@ public class TriggerEntity implements HasTriggerData { @Default @Embedded - private TriggerData data = new TriggerData(); + private TriggerEntity data = new TriggerEntity(); @Column(length = 200) private String runningOn; @@ -55,33 +56,43 @@ public class TriggerEntity implements HasTriggerData { @Nullable private OffsetDateTime lastPing; - public TriggerEntity(TriggerKey key, String correlationId) { - if (this.data == null) this.data = new TriggerData(); + public RunningTriggerEntity(TriggerKey key) { + if (this.data == null) this.data = new TriggerEntity(); this.data.setKey(key); - this.data.setCorrelationId(correlationId); } public TriggerKey getKey() { if (data == null) return null; return data.getKey(); } + + /** + * @param e Sets either {@link TriggerStatus#SUCCESS} or {@link TriggerStatus#FAILED} + * based if the {@link Exception} is null or not. + */ + public RunningTriggerEntity complete(Exception e) { + finishTriggerWithStatus(e == null ? TriggerStatus.SUCCESS : TriggerStatus.FAILED, e); + return this; + } - public TriggerEntity cancel(Exception e) { + public RunningTriggerEntity cancel(Exception e) { + finishTriggerWithStatus(TriggerStatus.CANCELED, e); + if (e == null) this.data.setExceptionName("PersistentTask canceled"); + return this; + } + + public void finishTriggerWithStatus(TriggerStatus status, Exception e) { this.data.setEnd(OffsetDateTime.now()); - this.data.setStatus(TriggerStatus.CANCELED); + this.data.updateRunningDuration(); + this.data.setStatus(status); - if (e == null) { - this.data.setExceptionName("PersistentTask canceled"); - } else { + if (e != null) { this.data.setExceptionName(e.getClass().getName()); this.data.setLastException(ExceptionUtils.getStackTrace(e)); } - - this.data.updateRunningDuration(); - return this; } - public TriggerEntity runOn(String runningOn) { + public RunningTriggerEntity runOn(String runningOn) { this.data.setStart(OffsetDateTime.now()); this.data.setEnd(null); this.data.setExecutionCount(data.getExecutionCount() + 1); @@ -92,32 +103,14 @@ public TriggerEntity runOn(String runningOn) { return this; } - /** - * @param e Sets either {@link TriggerStatus#SUCCESS} or {@link TriggerStatus#FAILED} - * based if the {@link Exception} is null or not. - */ - public TriggerEntity complete(Exception e) { - data.setStatus(TriggerStatus.SUCCESS); - data.setEnd(OffsetDateTime.now()); - data.updateRunningDuration(); - - if (e != null) { - data.setStatus(TriggerStatus.FAILED); - data.setExceptionName(e.getClass().getName()); - data.setLastException(ExceptionUtils.getStackTrace(e)); - } - - return this; - } - - public TriggerEntity runAt(OffsetDateTime runAt) { + public RunningTriggerEntity runAt(OffsetDateTime runAt) { data.setStatus(TriggerStatus.WAITING); data.setRunAt(runAt); setRunningOn(null); return this; } - public TriggerEntity withState(byte[] state) { + public RunningTriggerEntity withState(byte[] state) { this.data.setState(state); return this; } @@ -126,7 +119,7 @@ public boolean isWaiting() { return data.getStatus() == TriggerStatus.WAITING; } - public TriggerData copyData() { + public TriggerEntity copyData() { if (data == null) return null; return this.data.copy(); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/TriggerRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java similarity index 75% rename from core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/TriggerRepository.java rename to core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java index 6817d4d27..3750998c9 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/TriggerRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/repository/RunningTriggerRepository.java @@ -15,15 +15,16 @@ import org.springframework.data.repository.query.Param; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.shared.repository.TriggerDataRepository; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.shared.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import jakarta.persistence.LockModeType; import jakarta.persistence.QueryHint; -public interface TriggerRepository extends TriggerDataRepository { +public interface RunningTriggerRepository extends TriggerRepository { + @Query("SELECT e FROM #{#entityName} e WHERE e.data.key = :key") - Optional findByKey(@Param("key") TriggerKey key); + Optional findByKey(@Param("key") TriggerKey key); // https://jakarta.ee/specifications/persistence/3.0/jakarta-persistence-spec-3.0.html#a2132 @Lock(LockModeType.PESSIMISTIC_WRITE) @@ -37,7 +38,7 @@ public interface TriggerRepository extends TriggerDataRepository AND e.data.status = :status ORDER BY e.data.priority DESC, e.data.executionCount ASC """) - List loadNextTasks( + List loadNextTasks( @Param("runAt") OffsetDateTime runAt, @Param("status") TriggerStatus status, Pageable page); @@ -51,10 +52,10 @@ List loadNextTasks( SELECT e FROM #{#entityName} e WHERE e.data.key = :key """) - TriggerEntity lockByKey(@Param("key") TriggerKey key); + RunningTriggerEntity lockByKey(@Param("key") TriggerKey key); @Query(""" - UPDATE TriggerEntity + UPDATE RunningTriggerEntity SET lastPing = :lastPing, runningOn = :runningOn, data.status = :status WHERE data.key IN ( :keys ) """) @@ -70,6 +71,16 @@ int markTriggersAsRunning( SELECT e FROM #{#entityName} e WHERE e.lastPing < :lastPing """) - List findTriggersLastPingAfter( + List findTriggersLastPingAfter( @Param("lastPing") OffsetDateTime lastPing); + + @Query(""" + SELECT e FROM #{#entityName} e + WHERE e.data.status = :status + AND e.data.runAt <= :runAt + """) + List findByStatusAndRunAtAfter( + @Param("status") TriggerStatus status, + @Param("runAt") OffsetDateTime runAt, + Pageable page); } diff --git a/core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0130011aa..b44b68e8b 100644 --- a/core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -3,22 +3,38 @@ { "name": "spring.persistent-tasks.poll-rate", "type": "java.lang.Integer", - "description": "The interval at which the scheduler checks for new tasks in seconds." + "description": "The interval at which the scheduler checks for new tasks in seconds.", + "defaultValue": 60 + }, + { + "name": "spring.persistent-tasks.poll-abandoned-triggers", + "type": "java.lang.Integer", + "description": "The interval at which to check for abandoned triggers in seconds.", + "defaultValue": 300 + }, + { + "name": "spring.persistent-tasks.timers-enabled", + "type": "java.lang.Boolean", + "description": "If the times are enabled or not. Default true.", + "defaultValue": true }, { "name": "spring.persistent-tasks.max-threads", "type": "java.lang.Integer", - "description": "The number of threads to use; default 10; set to 0 to disable task processing." + "description": "The number of threads to use; set to 0 to disable task processing.", + "defaultValue": 10 }, { - "name": "spring.persistent-tasks.task-timeout", + "name": "spring.persistent-tasks.trigger-timeout", "type": "java.time.Duration", - "description": "The maximum time allowed for a task and scheduler to complete a task. Defaults to 5 minutes." + "description": "The maximum time allowed for a task and scheduler to complete a task. Defaults to 5 minutes.", + "defaultValue": "PT5M" }, { - "name": "spring.persistent-tasks.poll-task-timeout", + "name": "spring.persistent-tasks.poll-awaiting-trigger-timeout", "type": "java.lang.Integer", - "description": "The interval at which the system checks for abandoned tasks, in seconds. Defaults to every 5 minutes." + "description": "The interval at which the system checks for abandoned tasks, in seconds. Defaults to every 5 minutes.", + "defaultValue": 300 }, { "name": "spring.persistent-tasks.scheduler-enabled", diff --git a/core/src/test/java/org/sterl/spring/persistent_task/api/TaskIdTest.java b/core/src/test/java/org/sterl/spring/persistent_task/api/TaskIdTest.java new file mode 100644 index 000000000..39c9516ef --- /dev/null +++ b/core/src/test/java/org/sterl/spring/persistent_task/api/TaskIdTest.java @@ -0,0 +1,15 @@ +package org.sterl.spring.persistent_task.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.sterl.spring.persistent_tasks.api.TaskId.TriggerBuilder; + +class TaskIdTest { + + @Test + void test() { + assertThat(TriggerBuilder.newTrigger("foo").build().key().getId()).isNull(); + assertThat(TriggerBuilder.newTrigger("foo").build().key().getTaskName()).isEqualTo("foo"); + } +} diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/AbstractSpringTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/AbstractSpringTest.java index d7a3b06f0..5cd7518a1 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/AbstractSpringTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/AbstractSpringTest.java @@ -4,6 +4,7 @@ import java.time.Duration; import java.util.Optional; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @@ -14,7 +15,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.event.RecordApplicationEvents; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -25,11 +29,12 @@ import org.sterl.spring.persistent_tasks.scheduler.SchedulerService; import org.sterl.spring.persistent_tasks.scheduler.component.EditSchedulerStatusComponent; import org.sterl.spring.persistent_tasks.scheduler.config.SchedulerConfig; +import org.sterl.spring.persistent_tasks.scheduler.config.SchedulerThreadFactory; import org.sterl.spring.persistent_tasks.task.TaskService; import org.sterl.spring.persistent_tasks.test.AsyncAsserts; import org.sterl.spring.persistent_tasks.test.PersistentTaskTestService; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import org.sterl.spring.sample_app.SampleApp; import org.sterl.test.hibernate_asserts.HibernateAsserts; @@ -37,7 +42,7 @@ import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; -//@ActiveProfiles("mssql") // postgres mssql mariadb mysql +@ActiveProfiles // postgres mssql mariadb mysql @SpringBootTest(classes = SampleApp.class, webEnvironment = WebEnvironment.RANDOM_PORT) @RecordApplicationEvents public class AbstractSpringTest { @@ -62,6 +67,8 @@ public class AbstractSpringTest { @Autowired protected HistoryService historyService; + @Autowired + private ThreadPoolTaskExecutor triggerHistoryExecutor; @Autowired protected TransactionTemplate trx; @@ -91,25 +98,26 @@ HibernateAsserts hibernateAsserts(EntityManager entityManager) { @Primary @Bean("schedulerA") - @SuppressWarnings("resource") - SchedulerService schedulerA(TriggerService triggerService, + SchedulerService schedulerA( + TriggerService triggerService, MeterRegistry meterRegistry, EditSchedulerStatusComponent editSchedulerStatus, + SchedulerThreadFactory threadFactory, TransactionTemplate trx) throws UnknownHostException { final var name = "schedulerA"; - return SchedulerConfig.newSchedulerService(name, meterRegistry, triggerService, editSchedulerStatus, 10, Duration.ZERO, trx); + return SchedulerConfig.newSchedulerService(name, meterRegistry, triggerService, editSchedulerStatus, threadFactory, 10, Duration.ZERO, trx); } @Bean - @SuppressWarnings("resource") SchedulerService schedulerB(TriggerService triggerService, MeterRegistry meterRegistry, EditSchedulerStatusComponent editSchedulerStatus, + SchedulerThreadFactory threadFactory, TransactionTemplate trx) throws UnknownHostException { final var name = "schedulerB"; - return SchedulerConfig.newSchedulerService(name, meterRegistry, triggerService, editSchedulerStatus, 20, Duration.ZERO, trx); + return SchedulerConfig.newSchedulerService(name, meterRegistry, triggerService, editSchedulerStatus, threadFactory, 20, Duration.ZERO, trx); } /** @@ -137,7 +145,7 @@ public static class Task3 implements PersistentTask { private final AsyncAsserts asserts; @Override - public void accept(String state) { + public void accept(@Nullable String state) { try { Thread.sleep(1); } catch (InterruptedException e) { @@ -163,9 +171,13 @@ PersistentTask slowTask(AsyncAsserts asserts) { }; } } + + protected void awaitHistoryThreads() { + Awaitility.await().until(() -> triggerHistoryExecutor.getActiveCount() == 0); + } @Deprecated - protected Optional runNextTrigger() { + protected Optional runNextTrigger() { return persistentTaskTestService.runNextTrigger(); } @@ -185,6 +197,9 @@ public void beforeEach() throws Exception { public void afterEach() throws Exception { schedulerA.shutdownNow(); schedulerB.shutdownNow(); + + awaitHistoryThreads(); + triggerService.deleteAll(); historyService.deleteAll(); } 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 bc0a9989f..c9a08228b 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 @@ -13,8 +13,8 @@ import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3; import org.sterl.spring.persistent_tasks.PersistentTaskService; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerStatus; class HistoryServiceTest extends AbstractSpringTest { @@ -27,7 +27,7 @@ class HistoryServiceTest extends AbstractSpringTest { @Test void testReQueueTrigger() { // GIVEN - final AddTriggerRequest triggerRequest = Task3.ID.newUniqueTrigger("Hallo"); + final TriggerRequest triggerRequest = Task3.ID.newUniqueTrigger("Hallo"); var trigger = triggerService.run(triggerRequest, "test").get(); asserts.assertValue(Task3.NAME + "::Hallo"); @@ -73,9 +73,8 @@ void testTriggerHistoryTrx() { // THEN // 2 to get the work done - // 1 for the running history // 1 for the success history - hibernateAsserts.assertTrxCount(4); + hibernateAsserts.assertTrxCount(3); assertThat(subject.countTriggers(trigger.key())).isEqualTo(1); assertThat(subject.findAllDetailsForKey(trigger.key()).getTotalElements()).isEqualTo(3); } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepositoryTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepositoryTest.java similarity index 83% rename from core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepositoryTest.java rename to core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepositoryTest.java index 963e68e2b..623aa8702 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepositoryTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/CompletedTriggerRepositoryTest.java @@ -11,14 +11,14 @@ import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; -class TriggerHistoryLastStateRepositoryTest extends AbstractSpringTest { +class CompletedTriggerRepositoryTest extends AbstractSpringTest { final AtomicLong idGenerator = new AtomicLong(0); @Autowired - private TriggerHistoryLastStateRepository subject; + private CompletedTriggerRepository subject; @Test void testListTriggerStatus() { @@ -53,13 +53,13 @@ void testListTriggerStatus() { assertThat(result.get(i).executionCount()).isEqualTo(1L); } - private TriggerHistoryLastStateEntity createStatus(TriggerKey key, TriggerStatus status) { + private CompletedTriggerEntity createStatus(TriggerKey key, TriggerStatus status) { final var now = OffsetDateTime.now(); final var isCancel = status == TriggerStatus.CANCELED; - TriggerHistoryLastStateEntity result = new TriggerHistoryLastStateEntity(); + CompletedTriggerEntity result = new CompletedTriggerEntity(); result.setId(idGenerator.incrementAndGet()); - result.setData(TriggerData + result.setData(TriggerEntity .builder() .start(isCancel ? null : now.minusMinutes(1)) .end(isCancel ? null : now) 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 d8668e08c..c08da9a7d 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 @@ -14,10 +14,10 @@ import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3; import org.sterl.spring.persistent_tasks.PersistentTaskService; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.TaskId.TriggerBuilder; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.scheduler.entity.SchedulerEntity; @@ -118,7 +118,7 @@ void verifyRunningStatusTest() throws Exception { @Test void testRunOrQueue() throws Exception { // GIVEN - final AddTriggerRequest triggerRequest = Task3.ID + final TriggerRequest triggerRequest = Task3.ID .newTrigger("Hallo") .build(); @@ -136,7 +136,7 @@ void testRunOrQueue() throws Exception { @Test void testQueuedInFuture() { // GIVEN - final AddTriggerRequest triggerRequest = Task3.ID + final TriggerRequest triggerRequest = Task3.ID .newTrigger("Hallo") .runAfter(Duration.ofMinutes(5)) .build(); 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 941d93460..e30445992 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.lang.Nullable; import org.springframework.transaction.support.TransactionTemplate; import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.PersistentTaskService; @@ -43,7 +44,7 @@ static class Config { TransactionalTask savePersonInTrx(PersonRepository personRepository) { return new TransactionalTask() { @Override - public void accept(String name) { + public void accept(@Nullable String name) { personRepository.save(new PersonEntity(name)); COUNTDOWN.await(); if (sendError.get()) { @@ -61,7 +62,7 @@ PersistentTask savePersonNoTrx(TransactionTemplate trx, PersonRepository personRepository) { return new PersistentTask<>() { @Override - public void accept(String name) { + public void accept(@Nullable String name) { trx.executeWithoutResult(t -> { personRepository.save(new PersonEntity(name)); COUNTDOWN.await(); @@ -106,10 +107,9 @@ void testSaveNoTransactions() throws Exception { // THEN // 1. get the trigger // 2. one the event running - // 3. for the work - // 4. for success status - // 5. the history - hibernateAsserts.assertTrxCount(5); + // 3. for the work & for success status + // 4. the history + hibernateAsserts.assertTrxCount(4); assertThat(personRepository.count()).isOne(); // AND var data = persistentTaskService.getLastDetailData(trigger.key()); @@ -185,8 +185,7 @@ void testRunOrQueueTransactions() throws Exception { var k1 = subject.runOrQueue(TriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build()); // THEN 1 to save and 1 to start it and 1 for the history - Awaitility.await().until(() -> hibernateAsserts.getStatistics().getTransactionCount() > 2); - Thread.sleep(250); // wait for the history async events + awaitHistoryThreads(); hibernateAsserts.assertTrxCount(3); assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus()) .isEqualTo(TriggerStatus.RUNNING); @@ -195,6 +194,7 @@ void testRunOrQueueTransactions() throws Exception { hibernateAsserts.reset(); COUNTDOWN.countDown(); Awaitility.await().until(() -> hibernateAsserts.getStatistics().getTransactionCount() >= 1); + awaitHistoryThreads(); hibernateAsserts.assertTrxCount(1); // THEN 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 44891aaa8..87a0a1a04 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 @@ -50,7 +50,7 @@ void rescheduleAbandonedTasksTest() throws Exception { .isEqualTo(2); // WHEN - final var tasks = schedulerB.rescheduleAbandonedTasks(timeout); + final var tasks = schedulerB.rescheduleAbandonedTriggers(timeout); // THEN assertThat(tasks).hasSize(1); diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java index 238e4e1f5..1824805c1 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java @@ -6,10 +6,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; +import org.springframework.lang.Nullable; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -18,109 +18,136 @@ import org.sterl.spring.persistent_tasks.api.task.PersistentTask; import org.sterl.spring.persistent_tasks.api.task.TransactionalTask; import org.sterl.spring.persistent_tasks.task.util.ReflectionUtil; +import org.sterl.spring.persistent_tasks.test.AsyncAsserts; import org.sterl.spring.sample_app.person.PersonEntity; import org.sterl.spring.sample_app.person.PersonRepository; -import lombok.RequiredArgsConstructor; - class TaskTransactionTest extends AbstractSpringTest { - - @Component("transactionalClass") - @Transactional(timeout = 5, propagation = Propagation.MANDATORY) - @RequiredArgsConstructor - static class TransactionalClass implements PersistentTask { - private final PersonRepository personRepository; - @Override - public void accept(String name) { - personRepository.save(new PersonEntity(name)); - personRepository.save(new PersonEntity(name)); - } - } - - @Component("transactionalMethod") - @Transactional(timeout = 76, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ) - @RequiredArgsConstructor - static class TransactionalMethod implements PersistentTask { - private final PersonRepository personRepository; - @Transactional(timeout = 6, propagation = Propagation.MANDATORY, isolation = Isolation.REPEATABLE_READ) - @Override - public void accept(String name) { - personRepository.save(new PersonEntity(name)); - personRepository.save(new PersonEntity(name)); - } - } - /** * A closure cannot be annotated, so we use a anonymous class */ @Configuration + @ComponentScan("org.sterl.spring.persistent_tasks.task.test_class") static class Config { - @Bean("transactionalAnonymous") - PersistentTask transactionalAnonymous(PersonRepository personRepository) { + @Bean("persistentTaskAnnotated") + PersistentTask persistentTaskAnnotated(PersonRepository personRepository, AsyncAsserts asserts) { return new PersistentTask() { - @Transactional(timeout = 7, propagation = Propagation.REQUIRES_NEW) + // this will not work! + @Transactional(timeout = 8, propagation = Propagation.REQUIRES_NEW) + @Override + public void accept(@Nullable String name) { + personRepository.save(new PersonEntity(name)); + asserts.info(name); + } + }; + } + @Bean("transactionalTaskAnnotated") + PersistentTask transactionalTaskAnnotated(PersonRepository personRepository, AsyncAsserts asserts) { + return new TransactionalTask() { + // this will not work! + @Transactional(timeout = 9, propagation = Propagation.REQUIRES_NEW) @Override - public void accept(String name) { + public void accept(@Nullable String name) { personRepository.save(new PersonEntity(name)); + asserts.info(name); } }; } @Bean("transactionalClosure") - TransactionalTask transactionalClosure(PersonRepository personRepository) { + TransactionalTask transactionalClosure(PersonRepository personRepository, AsyncAsserts asserts) { return name -> { personRepository.save(new PersonEntity(name)); - personRepository.save(new PersonEntity(name)); + asserts.info(name); }; } } + @Autowired AsyncAsserts asserts; @Autowired TaskService subject; @Autowired PersonRepository personRepository; - @Autowired @Qualifier("transactionalClass") + @Autowired PersistentTask transactionalClass; - @Autowired @Qualifier("transactionalMethod") - PersistentTask transactionalMethod; - @Autowired @Qualifier("transactionalAnonymous") - PersistentTask transactionalAnonymous; + @Autowired + PersistentTask transactionalClassAndMethod; + @Autowired + PersistentTask requiresNewMethod; + + @Autowired + PersistentTask persistentTaskAnnotated; + @Autowired + PersistentTask transactionalTaskAnnotated; + @Autowired + PersistentTask transactionalClosure; + @Test void testFindTransactionAnnotation() { var a = ReflectionUtil.getAnnotation(transactionalClass, Transactional.class); assertThat(a).isNotNull(); assertThat(a.timeout()).isEqualTo(5); + assertThat(a.propagation()).isEqualTo(Propagation.MANDATORY); - a = ReflectionUtil.getAnnotation(transactionalMethod, Transactional.class); + a = ReflectionUtil.getAnnotation(transactionalClassAndMethod, Transactional.class); assertThat(a).isNotNull(); assertThat(a.timeout()).isEqualTo(6); + assertThat(a.propagation()).isEqualTo(Propagation.MANDATORY); + assertThat(a.isolation()).isEqualTo(Isolation.REPEATABLE_READ); - a = ReflectionUtil.getAnnotation(transactionalAnonymous, Transactional.class); + a = ReflectionUtil.getAnnotation(requiresNewMethod, Transactional.class); assertThat(a).isNotNull(); assertThat(a.timeout()).isEqualTo(7); + assertThat(a.propagation()).isEqualTo(Propagation.REQUIRES_NEW); + + + a = ReflectionUtil.getAnnotation(persistentTaskAnnotated, Transactional.class); + assertThat(a).isNotNull(); + assertThat(a.timeout()).isEqualTo(8); + assertThat(a.propagation()).isEqualTo(Propagation.REQUIRES_NEW); + + a = ReflectionUtil.getAnnotation(transactionalTaskAnnotated, Transactional.class); + assertThat(a).isNotNull(); + assertThat(a.timeout()).isEqualTo(9); + assertThat(a.propagation()).isEqualTo(Propagation.REQUIRES_NEW); + + a = ReflectionUtil.getAnnotation(transactionalClosure, Transactional.class); + assertThat(a).isNull(); } @Test void testGetTransactionTemplate() { - var a = subject.getTransactionTemplate(transactionalClass); - assertThat(a).isPresent(); - assertThat(a.get().getTimeout()).isEqualTo(5); - assertThat(a.get().getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); + var a = subject.getTransactionTemplateIfJoinable(transactionalClass).orElse(null); + assertThat(a).isNotNull(); + assertThat(a.getTimeout()).isEqualTo(5); + assertThat(a.getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); + + a = subject.getTransactionTemplateIfJoinable(transactionalClassAndMethod).orElse(null); + assertThat(a).isNotNull(); + assertThat(a.getTimeout()).isEqualTo(6); + assertThat(a.getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); + assertThat(a.getIsolationLevel()).isEqualTo(Isolation.REPEATABLE_READ.value()); + + a = subject.getTransactionTemplateIfJoinable(requiresNewMethod).orElse(null); + assertThat(a).isNull(); // cannot join requires new + - a = subject.getTransactionTemplate(transactionalMethod); - assertThat(a).isPresent(); - assertThat(a.get().getTimeout()).isEqualTo(6); - assertThat(a.get().getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); - assertThat(a.get().getIsolationLevel()).isEqualTo(Isolation.REPEATABLE_READ.value()); + a = subject.getTransactionTemplateIfJoinable(persistentTaskAnnotated).orElse(null); + assertThat(a).isNull(); // cannot join requires new - a = subject.getTransactionTemplate(transactionalAnonymous); - assertThat(a).isEmpty(); + a = subject.getTransactionTemplateIfJoinable(transactionalTaskAnnotated).orElse(null); + assertThat(a).isNull(); // cannot join requires new + + a = subject.getTransactionTemplateIfJoinable(transactionalClosure).orElse(null); + assertThat(a).isNotNull(); // the default one + assertThat(a.getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); } - @Test - void testRequiresNewHasOwnTransaction() { + @ParameterizedTest + @ValueSource(strings = {"transactionalTaskAnnotated", "persistentTaskAnnotated", "requiresNewMethod"}) + void testRequiresNewHasOwnTransaction(String task) { // GIVEN var t = triggerService.queue(TriggerBuilder - .newTrigger("transactionalAnonymous", "test").build()); + .newTrigger(task, task + "test").build()); // WHEN personRepository.deleteAllInBatch(); @@ -128,24 +155,35 @@ void testRequiresNewHasOwnTransaction() { triggerService.run(t).get(); // THEN - hibernateAsserts.assertTrxCount(4); + asserts.awaitValue(task + "test"); + // AND + awaitHistoryThreads(); + hibernateAsserts.assertTrxCount(3); + // AND assertThat(personRepository.count()).isEqualTo(1); } @ParameterizedTest - @ValueSource(strings = {"transactionalClass", "transactionalMethod", "transactionalClosure"}) + @ValueSource(strings = {"transactionalClass", "transactionalClassAndMethod", "transactionalClosure"}) void testTransactionalTask(String task) throws InterruptedException { // GIVEN + personRepository.deleteAllInBatch(); var t = triggerService.queue(TriggerBuilder - .newTrigger(task, "test").build()); - + .newTrigger(task, task).build()); + // WHEN - personRepository.deleteAllInBatch(); hibernateAsserts.reset(); triggerService.run(t).get(); - Thread.sleep(50); // TODO wait for the history running event + // THEN - hibernateAsserts.assertTrxCount(2); - assertThat(personRepository.count()).isEqualTo(2); + asserts.awaitValue(task); + awaitHistoryThreads(); + hibernateAsserts + // running trigger + //.assertDeletedCount(1) + // 1 running trigger, 3 history, 1 trigger completed + .assertInsertCount(4) + .assertTrxCount(2); + assertThat(personRepository.count()).isEqualTo(1); } } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/RequiresNewMethod.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/RequiresNewMethod.java new file mode 100644 index 000000000..c98f7b808 --- /dev/null +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/RequiresNewMethod.java @@ -0,0 +1,26 @@ +package org.sterl.spring.persistent_tasks.task.test_class; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.sterl.spring.persistent_tasks.api.task.PersistentTask; +import org.sterl.spring.persistent_tasks.test.AsyncAsserts; +import org.sterl.spring.sample_app.person.PersonEntity; +import org.sterl.spring.sample_app.person.PersonRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class RequiresNewMethod implements PersistentTask { + private final PersonRepository personRepository; + private final AsyncAsserts asserts; + + @Transactional(timeout = 7, propagation = Propagation.REQUIRES_NEW) + @Override + public void accept(@Nullable String name) { + personRepository.save(new PersonEntity(name)); + asserts.info(name); + } +} diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClass.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClass.java new file mode 100644 index 000000000..a2b5c8db4 --- /dev/null +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClass.java @@ -0,0 +1,26 @@ +package org.sterl.spring.persistent_tasks.task.test_class; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.sterl.spring.persistent_tasks.api.task.PersistentTask; +import org.sterl.spring.persistent_tasks.test.AsyncAsserts; +import org.sterl.spring.sample_app.person.PersonEntity; +import org.sterl.spring.sample_app.person.PersonRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@Transactional(timeout = 5, propagation = Propagation.MANDATORY) +@RequiredArgsConstructor +public class TransactionalClass implements PersistentTask { + private final PersonRepository personRepository; + private final AsyncAsserts asserts; + + @Override + public void accept(@Nullable String name) { + personRepository.save(new PersonEntity(name)); + asserts.info(name); + } +} diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClassAndMethod.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClassAndMethod.java new file mode 100644 index 000000000..05aa0a8c4 --- /dev/null +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/test_class/TransactionalClassAndMethod.java @@ -0,0 +1,28 @@ +package org.sterl.spring.persistent_tasks.task.test_class; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.sterl.spring.persistent_tasks.api.task.PersistentTask; +import org.sterl.spring.persistent_tasks.test.AsyncAsserts; +import org.sterl.spring.sample_app.person.PersonEntity; +import org.sterl.spring.sample_app.person.PersonRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@Transactional(timeout = 5) +@RequiredArgsConstructor +public class TransactionalClassAndMethod implements PersistentTask { + private final PersonRepository personRepository; + private final AsyncAsserts asserts; + + @Transactional(timeout = 6, propagation = Propagation.MANDATORY, isolation = Isolation.REPEATABLE_READ) + @Override + public void accept(@Nullable String name) { + personRepository.save(new PersonEntity(name)); + asserts.info(name); + } +} diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/test/AsyncAsserts.java b/core/src/test/java/org/sterl/spring/persistent_tasks/test/AsyncAsserts.java index c295dfcf2..d589ca725 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/test/AsyncAsserts.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/test/AsyncAsserts.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.assertj.core.api.ListAssert; @@ -25,14 +27,28 @@ public class AsyncAsserts { @Getter @Setter private Duration defaultTimeout = Duration.ofSeconds(3); - public synchronized void clear() { - values.clear(); - counts.clear(); + private final Lock lock = new ReentrantLock(true); + + public void clear() { + lock.lock(); + try { + values.clear(); + counts.clear(); + } finally { + lock.unlock(); + } } - public synchronized int add(String value) { + public int add(String value) { values.add(value); - final int count = getCount(value) + 1; - counts.put(value, count); + + lock.lock(); + int count; + try { + count = getCount(value) + 1; + counts.put(value, count); + } finally { + lock.unlock(); + } if (values.size() > maxStepCount) { throw new IllegalStateException("Flow has already more than " + maxStepCount + " steps, assuming error!"); } @@ -42,14 +58,15 @@ public synchronized int add(String value) { * @return how often this value has been already added ... */ public int info(String value) { - if (value == null) { - value= "[null]"; - } + if (value == null) value= "[null]"; int count; int size; - synchronized (values) { + lock.lock(); + try { count = this.add(value); size = values.size(); + } finally { + lock.unlock(); } System.err.println(size + ". " + value); return count; @@ -114,7 +131,7 @@ public void awaitOrdered(Runnable fn, String value, String... values) { } public ListAssert assertValue(String value) { - return assertThat(new ArrayList<>(values)).contains(value); + return assertThat(values).contains(value); } public void assertMissing(String value) { diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java b/core/src/test/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java index 51c64569b..1211bf502 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java @@ -19,7 +19,7 @@ import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.scheduler.SchedulerService; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -41,7 +41,7 @@ public class PersistentTaskTestService { * * @return next {@link TriggerKey} if found */ - public Optional runNextTrigger() { + public Optional runNextTrigger() { return triggerService.run(triggerService.lockNextTrigger("test")); } @@ -51,9 +51,9 @@ public Optional runNextTrigger() { * @param dueUntil date to also check for trigger in the future * @return the triggers executed, to directly check if they have been successful */ - public List runAllDueTrigger(OffsetDateTime dueUntil) { - var result = new ArrayList(); - List trigger; + public List runAllDueTrigger(OffsetDateTime dueUntil) { + var result = new ArrayList(); + List trigger; while ( (trigger = triggerService.lockNextTrigger("test", 1, dueUntil)).size() > 0 ) { var key = triggerService.run(trigger.getFirst()); if (key.isPresent()) result.add(key.get()); 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 133056dac..b2c9ad8c9 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 @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executors; @@ -17,22 +16,26 @@ import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3; import org.sterl.spring.persistent_tasks.PersistentTaskService; -import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.TaskId.TriggerBuilder; import org.sterl.spring.persistent_tasks.api.TriggerKey; +import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository; +import org.sterl.spring.persistent_tasks.history.repository.CompletedTriggerRepository; import org.sterl.spring.persistent_tasks.task.exception.CancelTaskException; import org.sterl.spring.persistent_tasks.task.exception.FailTaskNoRetryException; import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; 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; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.repository.RunningTriggerRepository; + +import com.github.f4b6a3.uuid.UuidCreator; class TriggerServiceTest extends AbstractSpringTest { @@ -42,11 +45,11 @@ class TriggerServiceTest extends AbstractSpringTest { @Autowired private TriggerService subject; @Autowired - private TriggerRepository triggerRepository; + private RunningTriggerRepository triggerRepository; @Autowired private TaskRepository taskRepository; @Autowired - private TriggerHistoryLastStateRepository triggerHistoryLastStateRepository; + private CompletedTriggerRepository completedTriggerRepository; @Autowired private ApplicationEvents events; @@ -80,7 +83,7 @@ void testAddTrigger() throws Exception { // one for the trigger and just one for the history hibernateAsserts.assertInsertCount(2); // AND - assertThat(triggerHistoryLastStateRepository.count()).isZero(); + assertThat(completedTriggerRepository.count()).isZero(); // AND assertThat(events.stream(TriggerAddedEvent.class).count()).isOne(); // AND @@ -101,12 +104,18 @@ void testCreateTrigger() { // WHEN subject.queue(taskId.newTrigger().build()); - subject.queue(taskId.newTrigger().build()); + var t2 = subject.queue(taskId.newTrigger("foo").build()); // THEN assertThat(subject.countTriggers(taskId)).isEqualTo(2); + assertThat(t2.getId()).isNotNull(); + assertThat(t2.getKey().getId()).isNotNull(); + assertThat(t2.getKey().getTaskName()).isNotNull(); + // AND assertThat(events.stream(TriggerAddedEvent.class).count()).isEqualTo(2); + var t = subject.get(t2.getKey()).get(); + assertThat(t.getData().getState()).isEqualTo(subject.getStateSerializer().serialize("foo")); } @Test @@ -265,7 +274,7 @@ void testTriggerPriority() throws Exception { var keys = triggers.stream() // .map(t -> subject.queue(t)) // - .map(TriggerEntity::getKey) // + .map(RunningTriggerEntity::getKey) // .toList(); // WHEN @@ -336,7 +345,7 @@ void testLockTrigger() throws Exception { } // WHEN - ArrayList >> lockInvocations = new ArrayList<>(); + ArrayList >> lockInvocations = new ArrayList<>(); for (int i = 1; i <= 100; ++i) { lockInvocations.add(() -> triggerService.run(triggerService.lockNextTrigger("test"))); } @@ -360,7 +369,7 @@ void testLockTrigger() throws Exception { @Test void testQueuedInFuture() { // GIVEN - final AddTriggerRequest triggerRequest = Task3.ID + final TriggerRequest triggerRequest = Task3.ID .newTrigger("Hallo") .runAfter(Duration.ofMinutes(5)) .build(); @@ -383,18 +392,18 @@ void testQueuedInFuture() { void testRescheduleAbandonedTasks() { // GIVEN var now = OffsetDateTime.now(); - var t1 = new TriggerEntity(new TriggerKey("fooTask"), UUID.randomUUID().toString()) - .runOn("fooScheduler"); + var t1 = new RunningTriggerEntity(new TriggerKey(UuidCreator.getTimeOrdered().toString(), "fooTask")) + .runOn("fooScheduler"); t1.setLastPing(now.minusSeconds(60)); triggerRepository.save(t1); - var t2 = new TriggerEntity(new TriggerKey("barTask"), UUID.randomUUID().toString()) + var t2 = new RunningTriggerEntity(new TriggerKey(UuidCreator.getTimeOrdered().toString(), "barTask")) .runOn("barScheduler"); t2.setLastPing(now.minusSeconds(58)); triggerRepository.save(t2); // WHEN - final var rescheduledTasks = subject.rescheduleAbandonedTasks(now.minusSeconds(59)); + final var rescheduledTasks = subject.rescheduleAbandoned(now.minusSeconds(59)); // THEN assertThat(rescheduledTasks).hasSize(1); @@ -405,8 +414,7 @@ void testRescheduleAbandonedTasks() { void testUnknownTriggersNoRetry() { // GIVEN var t = triggerRepository.save( - new TriggerEntity( - new TriggerKey("fooTask-unknown"), UUID.randomUUID().toString())); + new RunningTriggerEntity(new TriggerKey(UuidCreator.getTimeOrdered().toString(), "fooTask-unknown"))); // WHEN persistentTaskTestService.runNextTrigger(); @@ -419,8 +427,8 @@ void testUnknownTriggersNoRetry() { @Test void testBadStateNoRetry() { - var t = triggerRepository.save(new TriggerEntity( - new TriggerKey("slowTask"), UUID.randomUUID().toString() + var t = triggerRepository.save(new RunningTriggerEntity( + new TriggerKey(UuidCreator.getTimeOrdered().toString(), "slowTask") ).withState(new byte[] {12, 54}) ); @@ -437,7 +445,7 @@ void testBadStateNoRetry() { } @Test - void tesCancelRunningTrigger() { + void testCancelRunningTrigger() { // GIVEN TaskId taskId = taskService.replace("foo-cancel", c -> { throw new CancelTaskException(c); @@ -452,13 +460,13 @@ void tesCancelRunningTrigger() { assertThat(historyService.findLastKnownStatus(key1).get().status()).isEqualTo(TriggerStatus.CANCELED); // AND - assertThat(events.stream(TriggerCanceledEvent.class).count()).isOne(); - assertThat(events.stream(TriggerFailedEvent.class).count()).isZero(); - assertThat(events.stream(TriggerSuccessEvent.class).count()).isZero(); + assertThat(events.stream(TriggerCanceledEvent.class)).hasSize(1); + assertThat(events.stream(TriggerFailedEvent.class)).hasSize(0); + assertThat(events.stream(TriggerSuccessEvent.class)).hasSize(0); } @Test - void tesFailRunningTriggerNoRetry() { + void testFailRunningTriggerNoRetry() { // GIVEN TaskId taskId = taskService.replace("foo-fail", c -> { throw new FailTaskNoRetryException(c); @@ -477,4 +485,92 @@ void tesFailRunningTriggerNoRetry() { assertThat(events.stream(TriggerCanceledEvent.class).count()).isZero(); assertThat(events.stream(TriggerSuccessEvent.class).count()).isZero(); } + + @Test + void testAddTriggerAndWaitForSignal() { + // GIVEN + TaskId taskId = taskService.replace("foo", c -> asserts.info("foo")); + + // WHEN + var triggerKey = subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().plusDays(1)) + .build()).getKey(); + + // THEN + assertThat(persistentTaskTestService.runNextTrigger()).isEmpty(); + var t = subject.get(triggerKey).get(); + assertThat(t.getData().getStatus()).isEqualTo(TriggerStatus.AWAITING_SIGNAL); + } + + @Test + void testResumeWaitingTriggerForSignal() { + // GIVEN + TaskId taskId = taskService.replace("foo", asserts::info); + subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().plusDays(1)) + .state("foo bar") + .build()); + var triggerKey = subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().plusDays(1)) + .state("old state") + .build()).getKey(); + assertThat(persistentTaskTestService.runNextTrigger()).isEmpty(); + + // WHEN + subject.resume(TriggerBuilder.newTrigger(triggerKey, "new state").build()); + + // THEN + var t = subject.get(triggerKey).get(); + assertThat(t.getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + assertThat(t.getData().getState()).isEqualTo(subject.getStateSerializer().serialize("new state")); + + // WHEN + assertThat(persistentTaskTestService.runNextTrigger()).isPresent(); + + // THEN + asserts.awaitValueOnce("new state"); + asserts.assertMissing("old state"); + asserts.assertMissing("foo bar"); + assertThat(events.stream(TriggerResumedEvent.class).count()).isOne(); + // AND + assertThat(persistentTaskTestService.runNextTrigger()).isEmpty(); + } + + @Test + void testAwaitForSignalTriggersInTimeoutWillNotRun() { + // GIVEN + TaskId taskId = taskService.replace("foo", asserts::info); + subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().minusSeconds(1)) + .state("old state") + .build()).getKey(); + + // WHEN & THEN + assertThat(persistentTaskTestService.runNextTrigger()).isEmpty(); + // AND + asserts.assertMissing("old state"); + } + + @Test + void testExpireTimeoutTriggers() { + // GIVEN + TaskId taskId = taskService.replace("foo", asserts::info); + subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().plusMinutes(1)) + .state("old state") + .build()); + var trigger = subject.queue(taskId.newTrigger() + .waitForSignal(OffsetDateTime.now().minusSeconds(1)) + .state("foobar") + .build()); + + // WHEN + var expired = subject.expireTimeoutTriggers(); + + // WHEN + assertThat(expired).hasSize(1); + assertThat(trigger).isEqualTo(expired.get(0)); + // AND + assertThat(events.stream(TriggerExpiredEvent.class).count()).isOne(); + } } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResourceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResourceTest.java index f5e79c348..0723cee42 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResourceTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResourceTest.java @@ -20,16 +20,16 @@ import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerStatus; -import org.sterl.spring.persistent_tasks.shared.model.TriggerData; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.repository.RunningTriggerRepository; class TriggerResourceTest extends AbstractSpringTest { @LocalServerPort private int port; @Autowired - private TriggerRepository triggerRepository; + private RunningTriggerRepository triggerRepository; private String baseUrl; private final RestTemplate template = new RestTemplate(); @@ -75,7 +75,7 @@ void testSearchById() { // WHEN var response = template.exchange( - baseUrl + "?id=*" + key2.getId().substring(5, 30) + "*", + baseUrl + "?search=*" + key2.getId().substring(5, 30) + "*", HttpMethod.GET, null, String.class); @@ -85,7 +85,7 @@ void testSearchById() { // WHEN response = template.exchange( - baseUrl + "?id=" + key1.getId().substring(0, 30) + "*", + baseUrl + "?search=" + key1.getId().substring(0, 30) + "*", HttpMethod.GET, null, String.class); @@ -100,13 +100,13 @@ void testSearchById() { @Test void testSearchByCorrelationId() { // GIVEN - var t1 = triggerService.queue(TriggerBuilder.newTrigger("task1").correlationId(UUID.randomUUID().toString()).build()); + var t1 = triggerService.queue(TriggerBuilder.newTrigger("task1").correlationId("correlationId" + UUID.randomUUID().toString()).build()); var t2 = triggerService.queue(TriggerBuilder.newTrigger("task1").build()); // null - var t3 = triggerService.queue(TriggerBuilder.newTrigger("task2").correlationId(UUID.randomUUID().toString()).build()); + var t3 = triggerService.queue(TriggerBuilder.newTrigger("task2").correlationId("correlationId" + UUID.randomUUID().toString()).build()); // WHEN var response = template.exchange( - baseUrl + "?id=" + t3.getData().getCorrelationId().substring(0, 28) + "*", + baseUrl + "?search=" + t3.getData().getCorrelationId().substring(0, 30) + "*", HttpMethod.GET, null, String.class); @@ -182,12 +182,12 @@ void testUpdateRunAt() { assertThat(response.getBody().getKey()).isEqualTo(triggerKey); } - private TriggerEntity createStatus(TriggerKey key, TriggerStatus status) { + private RunningTriggerEntity createStatus(TriggerKey key, TriggerStatus status) { final var now = OffsetDateTime.now(); final var isCancel = status == TriggerStatus.CANCELED; - TriggerEntity result = new TriggerEntity(); - result.setData(TriggerData + RunningTriggerEntity result = new RunningTriggerEntity(); + result.setData(TriggerEntity .builder() .correlationId(UUID.randomUUID().toString()) .start(isCancel ? null : now.minusMinutes(1)) diff --git a/core/src/test/resources/application.yml b/core/src/test/resources/application.yml index 4eae4afb2..cb51a2e62 100644 --- a/core/src/test/resources/application.yml +++ b/core/src/test/resources/application.yml @@ -9,7 +9,7 @@ spring: persistent-tasks: scheduler-enabled: false - + timers-enabled: false liquibase: change-log: classpath:db/changelog/db.changelog-master.xml diff --git a/db/pom.xml b/db/pom.xml index 31997ed6a..273ae1507 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.7.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v5.xml b/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v5.xml new file mode 100644 index 000000000..c25fcb9c2 --- /dev/null +++ b/db/src/main/resources/spring-persistent-tasks/db/pt-changelog-v5.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/db/src/test/java/org/sterl/spring/persistent_tasks/db/ApplyLiquibaseTest.java b/db/src/test/java/org/sterl/spring/persistent_tasks/db/ApplyLiquibaseTest.java index f3403e30b..9d47293f0 100644 --- a/db/src/test/java/org/sterl/spring/persistent_tasks/db/ApplyLiquibaseTest.java +++ b/db/src/test/java/org/sterl/spring/persistent_tasks/db/ApplyLiquibaseTest.java @@ -55,13 +55,13 @@ void test() throws Exception { Map.of("TABLE_NAME", "PT_SCHEDULER", "TABLE_SCHEMA", "PUBLIC")); assertThat(tables).contains( - Map.of("TABLE_NAME", "PT_TASK_TRIGGERS", + Map.of("TABLE_NAME", "PT_RUNNING_TRIGGERS", "TABLE_SCHEMA", "PUBLIC")); assertThat(tables).contains( - Map.of("TABLE_NAME", "PT_TRIGGER_HISTORY_DETAILS", + Map.of("TABLE_NAME", "PT_COMPLETED_TRIGGERS", "TABLE_SCHEMA", "PUBLIC")); assertThat(tables).contains( - Map.of("TABLE_NAME", "PT_TRIGGER_HISTORY_LAST_STATES", + Map.of("TABLE_NAME", "PT_TRIGGER_HISTORY", "TABLE_SCHEMA", "PUBLIC")); } diff --git a/example/pom.xml b/example/pom.xml index b00dd3cad..4f3fcc410 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.8 + 3.3.12 @@ -14,6 +14,9 @@ Sample project for the Spring Persistent Tasks + 1.7.1-SNAPSHOT diff --git a/example/src/main/java/org/sterl/spring/example_app/shared/model/AbstractEntity.java b/example/src/main/java/org/sterl/spring/example_app/shared/model/AbstractEntity.java index c613c9400..637c2bcf4 100644 --- a/example/src/main/java/org/sterl/spring/example_app/shared/model/AbstractEntity.java +++ b/example/src/main/java/org/sterl/spring/example_app/shared/model/AbstractEntity.java @@ -5,6 +5,7 @@ import jakarta.persistence.Transient; public abstract class AbstractEntity implements Serializable { + private static final long serialVersionUID = 1L; public abstract T getId(); diff --git a/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Engine.java b/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Engine.java index 6c7f4f119..082accd5b 100644 --- a/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Engine.java +++ b/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Engine.java @@ -16,6 +16,7 @@ @Setter @NoArgsConstructor public class Engine extends AbstractEntity { + private static final long serialVersionUID = 1L; @GeneratedValue(strategy = GenerationType.SEQUENCE) @Id diff --git a/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Vehicle.java b/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Vehicle.java index 451ee9c76..377fb0a49 100644 --- a/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Vehicle.java +++ b/example/src/main/java/org/sterl/spring/example_app/vehicle/model/Vehicle.java @@ -21,6 +21,7 @@ @NoArgsConstructor @ToString public class Vehicle extends AbstractEntity { + private static final long serialVersionUID = 1L; @GeneratedValue(strategy = GenerationType.SEQUENCE) @Id diff --git a/example/src/main/java/org/sterl/spring/example_app/vehicle/task/BuildVehicleTask.java b/example/src/main/java/org/sterl/spring/example_app/vehicle/task/BuildVehicleTask.java index 2458a0e9d..17c0005c3 100644 --- a/example/src/main/java/org/sterl/spring/example_app/vehicle/task/BuildVehicleTask.java +++ b/example/src/main/java/org/sterl/spring/example_app/vehicle/task/BuildVehicleTask.java @@ -2,6 +2,7 @@ import java.util.Random; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.sterl.spring.example_app.vehicle.model.Vehicle; @@ -24,7 +25,7 @@ public class BuildVehicleTask implements TransactionalTask { @Transactional(timeout = 5) @Override - public void accept(Vehicle vehicle) { + public void accept(@Nullable Vehicle vehicle) { vehicleRepository.save(vehicle); log.info("Create vehicle ={}", vehicle); try { diff --git a/example/src/main/java/org/sterl/spring/example_app/vehicle/task/FailingBuildVehicleTask.java b/example/src/main/java/org/sterl/spring/example_app/vehicle/task/FailingBuildVehicleTask.java index b82d90852..c02cc9c06 100644 --- a/example/src/main/java/org/sterl/spring/example_app/vehicle/task/FailingBuildVehicleTask.java +++ b/example/src/main/java/org/sterl/spring/example_app/vehicle/task/FailingBuildVehicleTask.java @@ -2,6 +2,7 @@ import java.util.Random; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.sterl.spring.example_app.vehicle.model.Vehicle; import org.sterl.spring.example_app.vehicle.repository.VehicleRepository; @@ -22,7 +23,7 @@ public class FailingBuildVehicleTask implements TransactionalTask { private final VehicleRepository vehicleRepository; @Override - public void accept(Vehicle vehicle) { + public void accept(@Nullable Vehicle vehicle) { vehicleRepository.save(vehicle); log.info("Create vehicle with {} - which will fail", vehicle); try { diff --git a/pom.xml b/pom.xml index d70475c58..6f0f976a2 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.7.1-SNAPSHOT + 2.0.0-SNAPSHOT pom 2024 @@ -58,6 +58,7 @@ UTF-8 UTF-8 7.13.0 + 5.1.0 diff --git a/test/pom.xml b/test/pom.xml index 3d8fa694e..93325076d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.7.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/test/src/main/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java b/test/src/main/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java index 51c64569b..1211bf502 100644 --- a/test/src/main/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java +++ b/test/src/main/java/org/sterl/spring/persistent_tasks/test/PersistentTaskTestService.java @@ -19,7 +19,7 @@ import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.scheduler.SchedulerService; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -41,7 +41,7 @@ public class PersistentTaskTestService { * * @return next {@link TriggerKey} if found */ - public Optional runNextTrigger() { + public Optional runNextTrigger() { return triggerService.run(triggerService.lockNextTrigger("test")); } @@ -51,9 +51,9 @@ public Optional runNextTrigger() { * @param dueUntil date to also check for trigger in the future * @return the triggers executed, to directly check if they have been successful */ - public List runAllDueTrigger(OffsetDateTime dueUntil) { - var result = new ArrayList(); - List trigger; + public List runAllDueTrigger(OffsetDateTime dueUntil) { + var result = new ArrayList(); + List trigger; while ( (trigger = triggerService.lockNextTrigger("test", 1, dueUntil)).size() > 0 ) { var key = triggerService.run(trigger.getFirst()); if (key.isPresent()) result.add(key.get()); diff --git a/ui/pom.xml b/ui/pom.xml index 3e0a8c6ee..3bb0dd437 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.7.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml @@ -97,6 +97,7 @@ -- --run + ${skipTests} diff --git a/ui/src/server-api.d.ts b/ui/src/server-api.d.ts index 3b9eb7c73..bf63dd612 100644 --- a/ui/src/server-api.d.ts +++ b/ui/src/server-api.d.ts @@ -16,33 +16,13 @@ export interface SchedulerEntity { lastPing: string; } -export interface AddTriggerRequest { - key: TriggerKey; - state: T; - runtAt: string; - priority: number; - correlationId: string; -} - export interface RetryStrategy { } -export interface FixedIntervalRetryStrategy extends RetryStrategy { -} - -export interface LinearRetryStrategy extends RetryStrategy { -} - -export interface MultiplicativeRetryStrategy extends RetryStrategy { -} - -export interface TaskId extends Serializable { +export interface TaskId { name: string; } -export interface TriggerBuilder { -} - export interface TaskStatusHistoryOverview { taskName: string; status: TriggerStatus; @@ -59,6 +39,7 @@ export interface Trigger { id: number; instanceId: number; key: TriggerKey; + tag: string; correlationId: string; runningOn: string; createdTime: string; @@ -75,36 +56,28 @@ export interface Trigger { lastException: string; } -export interface TriggerKey extends Serializable { +export interface TriggerKey { id: string; taskName: string; } -export interface TriggerKeyBuilder { -} - -export interface PersistentTasksEvent { -} - -export interface TriggerTaskCommand extends PersistentTasksEvent { - triggers: AddTriggerRequest[]; -} - -export interface PersistentTask { - transactional: boolean; -} - -export interface RunningTrigger { +export interface TriggerRequest { key: TriggerKey; + status: TriggerStatus; + state: T; + runtAt: string; + priority: number; correlationId: string; - executionCount: number; - data: T; -} - -export interface RunningTriggerContextHolder { + tag: string; } -export interface TransactionalTask extends PersistentTask { +export interface TriggerSearch { + search: string; + keyId: string; + taskName: string; + correlationId: string; + status: TriggerStatus; + tag: string; } export interface PageMetadata { @@ -114,7 +87,4 @@ export interface PageMetadata { totalPages: number; } -export interface Serializable { -} - -export type TriggerStatus = "WAITING" | "RUNNING" | "SUCCESS" | "FAILED" | "CANCELED"; +export type TriggerStatus = "AWAITING_SIGNAL" | "WAITING" | "RUNNING" | "SUCCESS" | "FAILED" | "CANCELED" | "EXPIRED_SIGNAL"; diff --git a/ui/src/shared/date.util.ts b/ui/src/shared/date.util.ts index deb51a47d..146b1119e 100644 --- a/ui/src/shared/date.util.ts +++ b/ui/src/shared/date.util.ts @@ -67,4 +67,10 @@ export function formatMs(ms?: number) { const inDays = Math.floor(inHours / 24); return sign + inDays + "d " + (inHours % 24) + "h"; +} + +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 diff --git a/ui/src/shared/view/labled-text.view.tsx b/ui/src/shared/view/labled-text.view.tsx index f53cdcd6d..9098a41bc 100644 --- a/ui/src/shared/view/labled-text.view.tsx +++ b/ui/src/shared/view/labled-text.view.tsx @@ -1,18 +1,27 @@ -import { Form } from "react-bootstrap"; import React, { ReactNode } from "react"; +import { Form } from "react-bootstrap"; interface Props { label: string; value?: string | number | ReactNode; className?: string; + onClick?: () => void; } -const LabeledText: React.FC = ({ label, value, className }) => { +const LabeledText: React.FC = ({ label, value, className, onClick }) => { return ( {label} -
{value}
+ {onClick ? ( + + ) : ( +
{value}
+ )}
); }; diff --git a/ui/src/shared/view/trigger-list-item.view.tsx b/ui/src/shared/view/trigger-list-item.view.tsx index 0b33a88d5..61d2dc485 100644 --- a/ui/src/shared/view/trigger-list-item.view.tsx +++ b/ui/src/shared/view/trigger-list-item.view.tsx @@ -1,7 +1,7 @@ -import TriggerHistoryListView from "@src/history/view/trigger-history.view"; import { Trigger } from "@src/server-api"; import LabeledText from "@src/shared/view/labled-text.view"; -import JsonView from "@uiw/react-json-view"; +import { useUrl } from "crossroad"; +import { useEffect } from "react"; import { Accordion, Badge, @@ -12,22 +12,22 @@ import { Row, } from "react-bootstrap"; import TriggerStatusView from "../../trigger/views/trigger-staus.view"; -import { formatMs, formatShortDateTime } from "../date.util"; +import { formatMs, formatShortDateTime, runningSince } from "../date.util"; import { useServerObject } from "../http-request"; import HttpErrorView from "./http-error.view"; -import StackTraceView from "./stacktrace-view"; -import { useUrl } from "crossroad"; -import { useEffect } from "react"; +import TriggerView from "./trigger.view"; interface TriggerProps { trigger: Trigger; afterTriggerChanged?: () => void; showReRunButton: boolean; + onFieldClick: (key: string, value?: string) => void; } -const TriggerItemView = ({ +const TriggerListItemView = ({ trigger, afterTriggerChanged, showReRunButton, + onFieldClick, }: TriggerProps) => { // className="d-flex justify-content-between align-items-center" const [url, setUrl] = useUrl(); @@ -54,10 +54,7 @@ const TriggerItemView = ({ }, [setUrl, reRunTrigger.data]); return ( - triggerHistory.doGet()} - > + - + triggerHistory.doGet()}> ) : undefined} - ); }; -export default TriggerItemView; - -function runningSince(value?: string) { - if (!value) return ""; - const msRuntime = new Date().getTime() - new Date(value).getTime(); - return `since ${formatMs(msRuntime)}`; -} +export default TriggerListItemView; const TriggerCompactView = ({ trigger }: { trigger: Trigger }) => ( @@ -143,10 +135,17 @@ const TriggerCompactView = ({ trigger }: { trigger: Trigger }) => (
{trigger.key.taskName}
- + {isActive(trigger) ? ( + + ) : ( + + )} @@ -192,101 +191,6 @@ const TriggerExecutiomView = ({ trigger }: { trigger: Trigger }) => { ); }; -const TriggerDetailsView = ({ - trigger, - history, -}: { - trigger: Trigger; - history?: Trigger[]; -}) => { - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {trigger.lastPing ? ( - - - - - - ) : undefined} - - - - - -
- - - {isObject(trigger.state) ? ( - - ) : ( -
{trigger.state}
- )} - - - - -
- - ); -}; - -function isObject(value: any): boolean { - if (value === undefined || value === null) return false; - return typeof value === "object" || Array.isArray(value); +function isActive(trigger: Trigger): boolean { + return trigger.status == "RUNNING" || trigger.status == "WAITING"; } diff --git a/ui/src/shared/view/trigger-search.view.tsx b/ui/src/shared/view/trigger-search.view.tsx index 8c8b5d5eb..a6e07235b 100644 --- a/ui/src/shared/view/trigger-search.view.tsx +++ b/ui/src/shared/view/trigger-search.view.tsx @@ -8,7 +8,8 @@ import TriggerStatusSelect from "@src/shared/view/triger-status-select.view"; import TaskSelect from "@src/task/view/task-select.view"; import { useQuery } from "crossroad"; import { Accordion, Col, Form, Row, Stack } from "react-bootstrap"; -import TriggerItemView from "./trigger-list-item.view"; +import TriggerListItemView from "./trigger-list-item.view"; +import { useEffect, useState } from "react"; interface Props { url: string; @@ -21,13 +22,26 @@ const TriggersSearchView = ({ showReRunButton, }: Props) => { const [query, setQuery] = useQuery(); + const [search, setSearch] = useState(query.search || ""); const triggers = useServerObject>(url); + useEffect(() => { + setSearch(query.search || ""); + }, [query]); + const doReload = () => { return triggers.doGet( "?size=10&" + new URLSearchParams(query).toString() ); }; + const doUpdateQuery = (search: object) => { + setQuery((prev) => ({ + ...prev, + page: 0 + "", + ...search, + })); + }; + useAutoRefresh(10000, doReload, [query]); return ( @@ -36,29 +50,19 @@ const TriggersSearchView = ({ setSearch(e.target.value)} type="text" placeholder="ID search, '*' any string, '_' any character ..." onKeyUp={(e) => - e.key == "Enter" - ? setQuery((prev) => ({ - ...prev, - page: 0 + "", - id: (e.target as HTMLInputElement).value, - })) - : null + e.key == "Enter" ? doUpdateQuery({ search }) : null } /> - setQuery((prev) => ({ - ...prev, - status, - })) - } + onTaskChange={(status) => doUpdateQuery({ status })} /> @@ -66,12 +70,7 @@ const TriggersSearchView = ({ - setQuery((prev) => ({ - ...prev, - taskName: taskName, - })) - } + onTaskChange={(taskName) => doUpdateQuery({ taskName })} /> @@ -95,13 +94,14 @@ const TriggersSearchView = ({
{triggers.data?.content.map((t) => ( - doUpdateQuery({ [k]: v })} /> ))} diff --git a/ui/src/shared/view/trigger.view.tsx b/ui/src/shared/view/trigger.view.tsx new file mode 100644 index 000000000..6548342d0 --- /dev/null +++ b/ui/src/shared/view/trigger.view.tsx @@ -0,0 +1,128 @@ +import TriggerHistoryListView from "@src/history/view/trigger-history.view"; +import { Trigger } from "@src/server-api"; +import JsonView from "@uiw/react-json-view"; +import { Col, Row } from "react-bootstrap"; +import { formatMs, formatShortDateTime, runningSince } from "../date.util"; +import LabeledText from "./labled-text.view"; +import StackTraceView from "./stacktrace-view"; + +const TriggerView = ({ + trigger, + history, + onClick, +}: { + trigger: Trigger; + history?: Trigger[]; + onClick: (key: string, value?: string) => void; +}) => { + return ( + <> + + + onClick("search", trigger.key.id)} + /> + + + + onClick("taskName", trigger.key.taskName) + } + /> + + + + + + + + onClick("search", trigger.key.id)} + /> + + + onClick("search", trigger.key.id)} + /> + + + + + + + + + + + + + + + + + {trigger.lastPing ? ( + + + + + + ) : undefined} + + + + + +
+ + + {isObject(trigger.state) ? ( + + ) : ( +
{trigger.state}
+ )} + + + + +
+ + ); +}; + +export default TriggerView; + +function isObject(value: any): boolean { + if (value === undefined || value === null) return false; + return typeof value === "object" || Array.isArray(value); +} diff --git a/ui/src/task/view/task-select.view.tsx b/ui/src/task/view/task-select.view.tsx index 51d62496a..9e515482d 100644 --- a/ui/src/task/view/task-select.view.tsx +++ b/ui/src/task/view/task-select.view.tsx @@ -34,10 +34,10 @@ function TaskSelect({ value = "", onTaskChange }: TaskSelectProps) { return ( - + Task - +