From f3ff1d77e83fcfcafb2edb9e93d48bb78a5e5ff5 Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Fri, 10 Jan 2025 09:37:12 +0100
Subject: [PATCH 1/9] events now in transactions
---
.../spring/persistent_tasks/api/Trigger.java | 2 --
.../{shared/model => api}/TriggerStatus.java | 2 +-
.../history/HistoryService.java | 2 +-
.../component/TriggerHistoryComponent.java | 27 +++++++++----------
.../scheduler/SchedulerService.java | 9 +++++--
.../shared/model/HasTriggerData.java | 8 ++++++
.../shared/model/TriggerData.java | 5 ++++
.../repository/TriggerDataRepository.java | 2 +-
.../trigger/TriggerService.java | 2 +-
.../component/EditTriggerComponent.java | 13 +++++----
.../component/LockNextTriggerComponent.java | 2 +-
.../component/ReadTriggerComponent.java | 2 +-
.../component/RunTriggerComponent.java | 3 ++-
.../trigger/event/TriggerAddedEvent.java | 12 ++++++---
.../trigger/event/TriggerCanceledEvent.java | 10 +++++--
.../trigger/event/TriggerFailedEvent.java | 11 +++++---
.../trigger/event/TriggerLifeCycleEvent.java | 23 +++++++---------
.../trigger/event/TriggerRunningEvent.java | 10 +++++--
.../trigger/event/TriggerSuccessEvent.java | 11 +++++---
.../trigger/model/TriggerEntity.java | 7 ++++-
.../trigger/repository/TriggerRepository.java | 2 +-
.../persistent_tasks/AbstractSpringTest.java | 2 +-
.../TaskSchedulerServiceTest.java | 2 +-
.../history/HistoryServiceTest.java | 2 +-
.../TriggerHistoryDetailRepositoryTest.java | 2 +-
.../scheduler/SchedulerServiceTest.java | 2 +-
.../SchedulerServiceTransactionTest.java | 2 +-
.../scheduler/TaskFailoverTest.java | 2 +-
.../trigger/TriggerServiceTest.java | 2 +-
.../trigger/api/TriggerResourceTest.java | 2 +-
30 files changed, 114 insertions(+), 69 deletions(-)
rename core/src/main/java/org/sterl/spring/persistent_tasks/{shared/model => api}/TriggerStatus.java (85%)
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 689ea5be7..2e1120b4d 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/Trigger.java
@@ -2,8 +2,6 @@
import java.time.OffsetDateTime;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
-
import lombok.Data;
@Data
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerStatus.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java
similarity index 85%
rename from core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerStatus.java
rename to core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java
index 5c4bcd0ed..5f383df03 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/model/TriggerStatus.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerStatus.java
@@ -1,4 +1,4 @@
-package org.sterl.spring.persistent_tasks.shared.model;
+package org.sterl.spring.persistent_tasks.api;
import java.util.EnumSet;
import java.util.Set;
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 52e1aafa9..3f8345214 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
@@ -11,11 +11,11 @@
import org.springframework.data.domain.Sort.Direction;
import org.springframework.lang.Nullable;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
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.repository.TriggerHistoryDetailRepository;
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService;
import org.sterl.spring.persistent_tasks.trigger.TriggerService;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
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 c4ab6e68d..9e726eb77 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
@@ -10,7 +10,6 @@
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
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.model.TriggerEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -23,27 +22,25 @@ public class TriggerHistoryComponent {
private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository;
private final TriggerHistoryDetailRepository triggerHistoryDetailRepository;
- public void write(TriggerEntity e) {
+ @Transactional(timeout = 10)
+ @EventListener
+ public void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
+ log.debug("Received event={} for {} new status={}",
+ e.getClass().getSimpleName(),
+ e.key(), e.status());
+
+
var state = new TriggerHistoryLastStateEntity();
- state.setId(e.getId());
- state.setData(e.getData().toBuilder().build());
+ state.setId(e.id());
+ state.setData(e.getData().copy());
triggerHistoryLastStateRepository.save(state);
var detail = new TriggerHistoryDetailEntity();
- detail.setInstanceId(e.getId());
+ detail.setInstanceId(e.id());
detail.setData(e.getData().toBuilder()
.state(null)
+ .createdTime(OffsetDateTime.now())
.build());
- detail.getData().setCreatedTime(OffsetDateTime.now());
triggerHistoryDetailRepository.save(detail);
}
-
- @Transactional(timeout = 10)
- @EventListener
- public void onPersistentTaskEvent(TriggerLifeCycleEvent triggerLifeCycleEvent) {
- log.debug("Received event={} for {} new status={}",
- triggerLifeCycleEvent.getClass().getSimpleName(),
- triggerLifeCycleEvent.key(), triggerLifeCycleEvent.status());
- write(triggerLifeCycleEvent.trigger());
- }
}
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 b72014aaa..6225d9351 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
@@ -4,7 +4,9 @@
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import org.springframework.lang.NonNull;
@@ -44,6 +46,7 @@ public class SchedulerService {
private final TaskExecutorComponent taskExecutor;
private final EditSchedulerStatusComponent editSchedulerStatus;
private final TransactionTemplate trx;
+ private final Map shouldRun = new ConcurrentHashMap<>();
@PostConstruct
public void start() {
@@ -136,6 +139,7 @@ public TriggerKey runOrQueue(
if (taskExecutor.getFreeThreads() > 0) {
trigger = triggerService.markTriggersAsRunning(trigger, name);
pingRegistry().addRunning(1);
+ shouldRun.put(trigger.getId(), trigger);
} else {
log.debug("Currently not enough free thread available {} of {} in use. PersistentTask {} queued.",
taskExecutor.getFreeThreads(), taskExecutor.getMaxThreads(), trigger.getKey());
@@ -147,9 +151,10 @@ public TriggerKey runOrQueue(
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void checkIfTrigerIsRunning(TriggerAddedEvent addedTrigger) {
- if (addedTrigger.isRunningOn(name) && !taskExecutor.isRunning(addedTrigger.trigger())) {
+ final var toRun = shouldRun.remove(addedTrigger.id());
+ if (toRun != null) {
log.debug("New triger added for imidiate execution {}", addedTrigger.key());
- taskExecutor.submit(addedTrigger.trigger());
+ taskExecutor.submit(toRun);
}
}
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/HasTriggerData.java
index bdddf98b5..ce21166a2 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/HasTriggerData.java
@@ -4,10 +4,18 @@
import java.time.OffsetDateTime;
import org.sterl.spring.persistent_tasks.api.TaskId;
+import org.sterl.spring.persistent_tasks.api.TriggerKey;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
public interface HasTriggerData {
TriggerData getData();
+ default TriggerKey key() {
+ return getData().getKey();
+ }
+ default TriggerStatus status() {
+ return getData().getStatus();
+ }
default boolean isRunning() {
return getData().getStatus() == TriggerStatus.RUNNING;
}
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/TriggerData.java
index 1d3bfc607..42d68871c 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/TriggerData.java
@@ -4,6 +4,7 @@
import java.time.OffsetDateTime;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
@@ -86,4 +87,8 @@ public void updateRunningDuration() {
private String exceptionName;
@Lob
private String lastException;
+
+ public TriggerData 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
index 9aab4124d..daf4d49c0 100644
--- 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
@@ -11,8 +11,8 @@
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;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
@NoRepositoryBean
public interface TriggerDataRepository extends JpaRepository {
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java
index 2269b9ca5..0ddee5d26 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/TriggerService.java
@@ -14,7 +14,7 @@
import org.sterl.spring.persistent_tasks.api.AddTriggerRequest;
import org.sterl.spring.persistent_tasks.api.TaskId;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService;
import org.sterl.spring.persistent_tasks.task.TaskService;
import org.sterl.spring.persistent_tasks.trigger.component.EditTriggerComponent;
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 fca62afc3..81a1859f6 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
@@ -12,8 +12,8 @@
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.TriggerStatus;
import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
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.TriggerFailedEvent;
@@ -48,11 +48,12 @@ public Optional completeTaskWithStatus(TriggerKey key, Serializab
t.complete(e);
if (t.getData().getStatus() == TriggerStatus.SUCCESS) {
- publisher.publishEvent(new TriggerSuccessEvent(t, state));
+ publisher.publishEvent(new TriggerSuccessEvent(
+ t.getId(), t.copyData(), state));
log.debug("Setting {} to status={} {}", key, t.getData().getStatus(),
e == null ? "" : "error=" + e.getClass().getSimpleName());
} else {
- publisher.publishEvent(new TriggerFailedEvent(t, state, e));
+ publisher.publishEvent(new TriggerFailedEvent(t.getId(), t.copyData(), state, e));
log.info("Setting {} to status={} {}", key, t.getData().getStatus(),
e == null ? "" : "error=" + e.getClass().getSimpleName());
}
@@ -73,7 +74,8 @@ public Optional cancelTask(TriggerKey id) {
.findByKey(id) //
.map(t -> {
t.cancel();
- publisher.publishEvent(new TriggerCanceledEvent(t,
+ publisher.publishEvent(new TriggerCanceledEvent(
+ t.getId(), t.copyData(),
stateSerializer.deserializeOrNull(t.getData().getState())));
triggerRepository.delete(t);
return t;
@@ -94,7 +96,8 @@ public TriggerEntity addTrigger(AddTriggerRequest ti
result = triggerRepository.save(result);
log.debug("Added trigger={}", result);
}
- publisher.publishEvent(new TriggerAddedEvent(result, tigger.state()));
+ publisher.publishEvent(new TriggerAddedEvent(
+ result.getId(), result.copyData(), tigger.state()));
return result;
}
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 99e5fb1ae..d1c3a3d17 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
@@ -7,7 +7,7 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+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;
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 413d1653b..9fbdff8f1 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,7 +10,7 @@
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.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
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;
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 917950ffa..5a66453a5 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
@@ -73,7 +73,8 @@ Optional call() {
}
private Optional runTask() {
- eventPublisher.publishEvent(new TriggerRunningEvent(trigger, state));
+ eventPublisher.publishEvent(new TriggerRunningEvent(
+ trigger.getId(), trigger.copyData(), state, trigger.getRunningOn()));
persistentTask.accept(state);
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 5f8d1f1e8..4edfd5f9d 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,8 +2,14 @@
import java.io.Serializable;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
-
-public record TriggerAddedEvent(TriggerEntity trigger, Serializable state) implements TriggerLifeCycleEvent {
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
+
+/**
+ * Fired if a new trigger is added.
+ *
+ * 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 {
}
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 04c1f252d..36360709b 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,13 @@
import java.io.Serializable;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
-public record TriggerCanceledEvent(TriggerEntity trigger, Serializable state) implements TriggerLifeCycleEvent {
+/**
+ * Fired if a trigger could be canceled before it is running.
+ *
+ * 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 {
}
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 dd6a25fa5..cc8d0ad34 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
@@ -2,8 +2,13 @@
import java.io.Serializable;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
-
-public record TriggerFailedEvent(TriggerEntity trigger, Serializable state, Exception exception) implements TriggerLifeCycleEvent {
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
+
+/**
+ *
+ * Inside a transaction, it is save to join or listen for the AFTER_COMMIT
+ *
+ */
+public record TriggerFailedEvent(long id, TriggerData data, Serializable state, Exception exception) 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 59c1dad89..56951c19e 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
@@ -4,25 +4,20 @@
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
-import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
+import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData;
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
/**
- * Tag any events which are fired in case something changes on a trigger
+ * 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 {
- default TriggerKey key() {
- return trigger().getKey();
- }
- default TriggerStatus status() {
- return trigger().getData().getStatus();
- }
- default boolean isRunningOn(String name) {
- return trigger().isRunning() && name != null && name.equals(trigger().getRunningOn());
+public interface TriggerLifeCycleEvent extends HasTriggerData {
+ default TriggerData getData() {
+ return data();
}
+ long id();
@NonNull
- TriggerEntity trigger();
+ TriggerData data();
@Nullable
Serializable state();
}
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 c4a7fe411..3fd9377fa 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,11 +2,17 @@
import java.io.Serializable;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
/**
* Event fired before a trigger is executed
+ *
+ * This event is maybe not in a transaction and so a transactional event listener will not work.
+ *
*/
-public record TriggerRunningEvent(TriggerEntity trigger, Serializable state) implements TriggerLifeCycleEvent {
+public record TriggerRunningEvent(long id, TriggerData 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 51dc55e01..c33b35f77 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,8 +2,13 @@
import java.io.Serializable;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
-
-public record TriggerSuccessEvent(TriggerEntity trigger, Serializable state) implements TriggerLifeCycleEvent {
+import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
+
+/**
+ *
+ * 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 {
}
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/TriggerEntity.java
index d1de923e2..4abcfbc85 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/TriggerEntity.java
@@ -4,9 +4,9 @@
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.TriggerStatus;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
@@ -111,4 +111,9 @@ public TriggerEntity withState(byte[] state) {
this.data.setState(state);
return this;
}
+
+ public TriggerData 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/TriggerRepository.java
index da2c900f3..6817d4d27 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/TriggerRepository.java
@@ -14,7 +14,7 @@
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.query.Param;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+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;
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 87d174246..f70df765d 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
@@ -20,12 +20,12 @@
import org.springframework.transaction.support.TransactionTemplate;
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.api.TaskId;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.api.event.TriggerTaskCommand;
import org.sterl.spring.persistent_tasks.history.HistoryService;
import org.sterl.spring.persistent_tasks.scheduler.SchedulerService;
import org.sterl.spring.persistent_tasks.scheduler.component.EditSchedulerStatusComponent;
import org.sterl.spring.persistent_tasks.scheduler.component.TaskExecutorComponent;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
import org.sterl.spring.persistent_tasks.task.TaskService;
import org.sterl.spring.persistent_tasks.trigger.TriggerService;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/TaskSchedulerServiceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/TaskSchedulerServiceTest.java
index 42839c766..1deece043 100644
--- a/core/src/test/java/org/sterl/spring/persistent_tasks/TaskSchedulerServiceTest.java
+++ b/core/src/test/java/org/sterl/spring/persistent_tasks/TaskSchedulerServiceTest.java
@@ -9,7 +9,7 @@
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.api.RetryStrategy;
import org.sterl.spring.persistent_tasks.api.TaskId;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
class TaskSchedulerServiceTest extends AbstractSpringTest {
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 d0083e437..e7d48036a 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
@@ -12,7 +12,7 @@
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3;
import org.sterl.spring.persistent_tasks.api.AddTriggerRequest;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
class HistoryServiceTest extends AbstractSpringTest {
diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
index e82497ece..c9d3e6f6b 100644
--- a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
+++ b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
@@ -8,8 +8,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
class TriggerHistoryDetailRepositoryTest extends AbstractSpringTest {
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 9acaaadb0..281042897 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
@@ -15,8 +15,8 @@
import org.sterl.spring.persistent_tasks.api.TaskId;
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.scheduler.entity.SchedulerEntity;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
class SchedulerServiceTest extends AbstractSpringTest {
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 7a92eb0d5..f67df4d5b 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
@@ -16,7 +16,7 @@
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
import org.sterl.spring.persistent_tasks.api.TransactionalTask;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.sample_app.person.PersonBE;
import org.sterl.spring.sample_app.person.PersonRepository;
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 a1548637b..0efcd9d0c 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
@@ -9,7 +9,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
import org.sterl.spring.persistent_tasks.api.TaskId;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
class TaskFailoverTest extends AbstractSpringTest {
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 3656ee70a..a8dc4207a 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
@@ -18,7 +18,7 @@
import org.sterl.spring.persistent_tasks.api.TaskId;
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.task.repository.TaskRepository;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository;
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 bec8180c0..b468b3ff5 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
@@ -16,7 +16,7 @@
import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3;
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
import org.sterl.spring.persistent_tasks.api.Trigger;
-import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
+import org.sterl.spring.persistent_tasks.api.TriggerStatus;
class TriggerResourceTest extends AbstractSpringTest {
From ace87b9355159677122de7e775fe46f36ede7afb Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Fri, 10 Jan 2025 13:36:16 +0100
Subject: [PATCH 2/9] update
---
.../PersistentTaskService.java | 18 ++++++++--
.../component/TriggerHistoryComponent.java | 33 +++++++++++++++----
.../history/HistoryServiceTest.java | 18 ++++++++++
.../SchedulerServiceTransactionTest.java | 33 ++++++++++++++++---
4 files changed, 87 insertions(+), 15 deletions(-)
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 921ad22b4..e7c734531 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
@@ -60,12 +60,12 @@ void queue(TriggerTaskCommand extends Serializable> event) {
if (event.triggers().size() == 1) {
runOrQueue(event.triggers().iterator().next());
} else {
- queueAll(event.triggers());
+ queue(event.triggers());
}
}
/**
- * Queues the given triggers.
+ * Queues/updates the given triggers, if the {@link TriggerKey} is already present
*
* @param the state type
* @param triggers the triggers to add
@@ -73,12 +73,24 @@ void queue(TriggerTaskCommand extends Serializable> event) {
*/
@Transactional(timeout = 10)
@NonNull
- public List queueAll(Collection> triggers) {
+ public List queue(Collection> triggers) {
return triggers.stream() //
.map(t -> triggerService.queue(t)) //
.map(TriggerEntity::getKey) //
.toList();
}
+ /**
+ * Queues/updates the given trigger, if the {@link TriggerKey} is already present.
+ *
+ * @param the state type
+ * @param trigger the trigger to add
+ * @return the {@link TriggerKey}
+ */
+ @Transactional(timeout = 5)
+ @NonNull
+ public TriggerKey queue(AddTriggerRequest trigger) {
+ return triggerService.queue(trigger).getKey();
+ }
/**
* Runs the given trigger if a free threads are available
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 9e726eb77..4c31d9a09 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
@@ -4,12 +4,16 @@
import org.springframework.context.event.EventListener;
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.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.shared.stereotype.TransactionalCompontant;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerLifeCycleEvent;
+import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -22,22 +26,37 @@ public class TriggerHistoryComponent {
private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository;
private final TriggerHistoryDetailRepository triggerHistoryDetailRepository;
+ //@Transactional(timeout = 10)
+ //@EventListener
+ void onRunning(TriggerRunningEvent e) {
+ log.debug("Received event={} for {} new status={}",
+ e.getClass().getSimpleName(),
+ e.key(), e.status());
+
+ execute(e.id(), e.data());
+ }
+
@Transactional(timeout = 10)
@EventListener
- public void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
+ //@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+ void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
+ //if (e instanceof TriggerRunningEvent) return; // we have an own listener for that
log.debug("Received event={} for {} new status={}",
e.getClass().getSimpleName(),
e.key(), e.status());
-
- var state = new TriggerHistoryLastStateEntity();
- state.setId(e.id());
- state.setData(e.getData().copy());
+ execute(e.id(), e.data());
+ }
+
+ public void execute(final long triggerId, final TriggerData data) {
+ final var state = new TriggerHistoryLastStateEntity();
+ state.setId(triggerId);
+ state.setData(data.copy());
triggerHistoryLastStateRepository.save(state);
var detail = new TriggerHistoryDetailEntity();
- detail.setInstanceId(e.id());
- detail.setData(e.getData().toBuilder()
+ detail.setInstanceId(triggerId);
+ detail.setData(data.toBuilder()
.state(null)
.createdTime(OffsetDateTime.now())
.build());
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 e7d48036a..c57463b07 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
@@ -54,4 +54,22 @@ void testTriggerHistory() throws TimeoutException, InterruptedException {
assertThat(triggers.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING);
assertThat(triggers.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING);
}
+
+ @Test
+ void testTriggerHistoryTrx() throws TimeoutException, InterruptedException {
+ // GIVEN
+ final var trigger = Task3.ID.newUniqueTrigger("Hallo");
+ persistentTaskService.queue(trigger);
+ // WHEN
+ hibernateAsserts.reset();
+ schedulerService.triggerNextTasks().forEach(t -> {
+ try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
+ });
+
+ // THEN
+ // 2 to get the work
+ // 1 for the running history
+ // 1 for the success history
+ hibernateAsserts.assertTrxCount(4);
+ }
}
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 f67df4d5b..3c39502f5 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
@@ -3,7 +3,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -11,6 +13,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
+import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3;
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.api.RetryStrategy;
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
@@ -23,7 +26,8 @@
class SchedulerServiceTransactionTest extends AbstractSpringTest {
private SchedulerService subject;
- private static AtomicBoolean sendError = new AtomicBoolean(false);
+ private static final AtomicBoolean sendError = new AtomicBoolean(false);
+ private static final AtomicInteger sleepTime = new AtomicInteger(50);
@Autowired private PersonRepository personRepository;
@Configuration
@@ -34,10 +38,9 @@ TransactionalTask savePersonInTrx(PersonRepository personRepository) {
@Override
public void accept(String name) {
try {
- Thread.sleep(50);
+ if (sleepTime.intValue() > 0) Thread.sleep(sleepTime.intValue());
} catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ Thread.interrupted();
}
personRepository.save(new PersonBE(name));
if (sendError.get()) {
@@ -49,7 +52,7 @@ public RetryStrategy retryStrategy() {
}
};
}
-
+
@Bean
PersistentTask savePersonNoTrx(PersonRepository personRepository) {
return new PersistentTask<>() {
@@ -172,6 +175,26 @@ void testRollbackAndRetry() throws Exception {
assertExecutionCount(key, 2);
assertThat(personRepository.count()).isOne();
}
+
+ @Test
+ void testTriggerHistoryTrx() throws TimeoutException, InterruptedException {
+ // GIVEN
+ sleepTime.set(0);
+ final var trigger = Task3.ID.newUniqueTrigger("savePersonNoTrx");
+ persistentTaskService.queue(trigger);
+ // WHEN
+ hibernateAsserts.reset();
+ schedulerService.triggerNextTasks().forEach(t -> {
+ try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
+ });
+
+ // THEN
+ // 2 to get the work
+ // 1 for the running history
+ // 1 for the success history
+ hibernateAsserts.assertTrxCount(4);
+ }
+
private void assertExecutionCount(TriggerKey triggerKey, int count) throws InterruptedException, ExecutionException {
var data = persistentTaskService.getLastTriggerData(triggerKey);
From 16527daebe303051711c53260a1ad0e2ca9b5f8c Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Fri, 10 Jan 2025 18:40:28 +0100
Subject: [PATCH 3/9] adjusted transaction handling during the flow
---
.../PersistentTaskService.java | 7 +
.../SpringPersistentTasksConfig.java | 2 +
.../history/HistoryService.java | 8 +-
.../component/TriggerHistoryComponent.java | 20 ++-
.../TriggerHistoryLastStateRepository.java | 1 -
.../scheduler/SchedulerService.java | 62 ++++----
.../persistent_tasks/task/TaskService.java | 4 -
.../HandleTriggerExceptionComponent.java | 55 +++++++
.../component/RunTriggerComponent.java | 45 +-----
.../SchedulerServiceTransactionTest.java | 141 +++++++++++-------
.../task/TaskTransactionTest.java | 2 +-
.../test/java/org/sterl/test/Countdown.java | 26 ++++
12 files changed, 231 insertions(+), 142 deletions(-)
create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
create mode 100644 core/src/test/java/org/sterl/test/Countdown.java
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 e7c734531..c43a9d7f1 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
@@ -9,6 +9,7 @@
import java.util.concurrent.Future;
import org.springframework.context.event.EventListener;
+import org.springframework.data.domain.Pageable;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -55,6 +56,12 @@ public Optional getLastTriggerData(TriggerKey key) {
}
}
+ public Optional getLastDetailData(TriggerKey key) {
+ var data = historyService.findAllDetailsForKey(key, Pageable.ofSize(1));
+ if (data.isEmpty()) return Optional.empty();
+ return Optional.of(data.getContent().get(0).getData());
+ }
+
@EventListener
void queue(TriggerTaskCommand extends Serializable> event) {
if (event.triggers().size() == 1) {
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/SpringPersistentTasksConfig.java b/core/src/main/java/org/sterl/spring/persistent_tasks/SpringPersistentTasksConfig.java
index a2f7a70b8..39fcfb2c1 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/SpringPersistentTasksConfig.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/SpringPersistentTasksConfig.java
@@ -3,10 +3,12 @@
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
+@EnableAsync
@AutoConfigurationPackage(basePackageClasses = EnableSpringPersistentTasks.class)
@ComponentScan(basePackageClasses = EnableSpringPersistentTasks.class)
public class SpringPersistentTasksConfig {
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 3f8345214..0406cdecd 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
@@ -64,7 +64,7 @@ public Page findAllDetailsForKey(TriggerKey key) {
return findAllDetailsForKey(key, PageRequest.of(0, 100));
}
public Page findAllDetailsForKey(TriggerKey key, Pageable page) {
- page = sortByIdIfNeeded(page);
+ page = applyDefaultSortIfNeeded(page);
return triggerHistoryDetailRepository.listKnownStatusFor(key, page);
}
@@ -97,7 +97,7 @@ public long countTriggers(TriggerKey key) {
public Page findTriggerState(
@Nullable TriggerKey key, Pageable page) {
- page = sortByIdIfNeeded(page);
+ page = applyDefaultSortIfNeeded(page);
if (key == null) return triggerHistoryLastStateRepository.findAll(page);
if (key.getId() == null && key.getTaskName() == null) return triggerHistoryLastStateRepository.findAll(page);
if (key.getId() == null && key.getTaskName() != null) {
@@ -109,10 +109,10 @@ public Page findTriggerState(
page);
}
- private Pageable sortByIdIfNeeded(Pageable page) {
+ private Pageable applyDefaultSortIfNeeded(Pageable page) {
if (page.getSort() == Sort.unsorted()) {
return PageRequest.of(page.getPageNumber(), page.getPageSize(),
- Sort.by(Direction.DESC, "id"));
+ Sort.by(Direction.DESC, "data.createdTime", "id"));
}
return page;
}
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 4c31d9a09..7c93d633a 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
@@ -3,6 +3,7 @@
import java.time.OffsetDateTime;
import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@@ -26,9 +27,14 @@ public class TriggerHistoryComponent {
private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository;
private final TriggerHistoryDetailRepository triggerHistoryDetailRepository;
- //@Transactional(timeout = 10)
- //@EventListener
- void onRunning(TriggerRunningEvent e) {
+ // we have to ensure to run in an own transaction
+ // as if the trigger fails, a rollback would also remove this entry
+ // furthermore async to ensure that we would not block
+ // as REQURES_NEW would block two DB connections ...
+ @Async
+ @Transactional(timeout = 10)
+ @EventListener
+ public void onRunning(TriggerRunningEvent e) {
log.debug("Received event={} for {} new status={}",
e.getClass().getSimpleName(),
e.key(), e.status());
@@ -36,11 +42,11 @@ void onRunning(TriggerRunningEvent e) {
execute(e.id(), e.data());
}
- @Transactional(timeout = 10)
- @EventListener
- //@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+ // @Transactional(timeout = 10)
+ // @EventListener
+ @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
- //if (e instanceof TriggerRunningEvent) return; // we have an own listener for that
+ if (e instanceof TriggerRunningEvent) return; // we have an own listener for that
log.debug("Received event={} for {} new status={}",
e.getClass().getSimpleName(),
e.key(), e.status());
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/TriggerHistoryLastStateRepository.java
index 42239566a..5bc208d9c 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/TriggerHistoryLastStateRepository.java
@@ -4,5 +4,4 @@
public interface TriggerHistoryLastStateRepository extends HistoryTriggerRepository {
-
}
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 6225d9351..54626da77 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
@@ -30,11 +30,10 @@
import lombok.extern.slf4j.Slf4j;
/**
- * Use this service if direct access to the Scheduler is required.
- *
- * Note: This Service is optional, as it could be disabled if no background
- * tasks should be execute on this note. As so the {@link TriggerService} should be
- * preferred to queue tasks.
+ * Use this service if direct access to the Scheduler is required.
+ * Note: This Service is optional, as it could be disabled if no
+ * background tasks should be execute on this note. As so the
+ * {@link TriggerService} should be preferred to queue tasks.
*/
@RequiredArgsConstructor
@Slf4j
@@ -73,22 +72,20 @@ public void shutdownNow() {
editSchedulerStatus.offline(name);
}
+ @Transactional
public SchedulerEntity pingRegistry() {
- // using trx template to ensure the TRX is started if we use this method internally
- return trx.execute(t -> {
- var result = editSchedulerStatus.checkinToRegistry(name);
- result.setRunnungTasks(taskExecutor.getRunningTasks());
- result.setTasksSlotCount(taskExecutor.getMaxThreads());
- log.debug("Ping {}", result);
- return result;
- });
+ var result = editSchedulerStatus.checkinToRegistry(name);
+ result.setRunnungTasks(taskExecutor.getRunningTasks());
+ result.setTasksSlotCount(taskExecutor.getMaxThreads());
+ log.debug("Ping {}", result);
+ return result;
}
-
+
public SchedulerEntity getScheduler() {
var result = editSchedulerStatus.get(name);
return result;
}
-
+
public Optional findStatus(String name) {
return editSchedulerStatus.find(name);
}
@@ -102,8 +99,8 @@ public List> triggerNextTasks() {
}
/**
- * Like {@link #triggerNextTasks()} but allows to set the time e.g. to the future to trigger
- * tasks which wouldn't be triggered now.
+ * Like {@link #triggerNextTasks()} but allows to set the time e.g. to the
+ * future to trigger tasks which wouldn't be triggered now.
*
* This method should not be called in a transaction!
*
@@ -112,11 +109,10 @@ public List> triggerNextTasks() {
public List> triggerNextTasks(OffsetDateTime timeDue) {
if (taskExecutor.getFreeThreads() > 0) {
final var result = trx.execute(t -> {
- var triggers = triggerService.lockNextTrigger(name,
- taskExecutor.getFreeThreads(), timeDue);
- pingRegistry().addRunning(triggers.size());
- return triggers;
- });
+ var triggers = triggerService.lockNextTrigger(name, taskExecutor.getFreeThreads(), timeDue);
+ pingRegistry().addRunning(triggers.size());
+ return triggers;
+ });
return taskExecutor.submit(result);
} else {
@@ -126,13 +122,14 @@ public List> triggerNextTasks(OffsetDateTime timeDue) {
}
/**
- * Runs the given trigger if a free threads are available
- * and the runAt time is not in the future.
- * @return the reference to the {@link Future} with the key, if no threads are available it is resolved
+ * Runs the given trigger if a free threads are available and the runAt time is
+ * not in the future.
+ *
+ * @return the reference to the {@link Future} with the key, if no threads are
+ * available it is resolved
*/
@Transactional(timeout = 10)
- public TriggerKey runOrQueue(
- AddTriggerRequest triggerRequest) {
+ public TriggerKey runOrQueue(AddTriggerRequest triggerRequest) {
var trigger = triggerService.queue(triggerRequest);
if (!trigger.shouldRunInFuture()) {
@@ -141,14 +138,14 @@ public TriggerKey runOrQueue(
pingRegistry().addRunning(1);
shouldRun.put(trigger.getId(), trigger);
} else {
- log.debug("Currently not enough free thread available {} of {} in use. PersistentTask {} queued.",
+ log.debug("Currently not enough free thread available {} of {} in use. PersistentTask {} queued.",
taskExecutor.getFreeThreads(), taskExecutor.getMaxThreads(), trigger.getKey());
}
}
// we will listen for the commit event to execute this trigger ...
return trigger.getKey();
}
-
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void checkIfTrigerIsRunning(TriggerAddedEvent addedTrigger) {
final var toRun = shouldRun.remove(addedTrigger.id());
@@ -166,14 +163,11 @@ public SchedulerEntity getStatus() {
public List rescheduleAbandonedTasks(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(TriggerEntity::getKey)
.toList();
int running = triggerService.markTriggersAsRunning(runningKeys, name);
- log.debug("({}) - {} trigger(s) are running on {} schedulers",
- running, runningKeys, schedulers);
+ log.debug("({}) - {} trigger(s) are running on {} schedulers", running, runningKeys, schedulers);
return triggerService.rescheduleAbandonedTasks(timeout);
}
}
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 9591d6cca..a37f75a07 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
@@ -5,10 +5,8 @@
import java.util.Set;
import java.util.function.Consumer;
-import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.api.TaskId;
@@ -18,14 +16,12 @@
import lombok.RequiredArgsConstructor;
@Service
-@DependsOnDatabaseInitialization
@RequiredArgsConstructor
public class TaskService {
private final TaskTransactionComponent taskTransactionComponent;
private final TaskRepository taskRepository;
- @Transactional(readOnly = true)
public Set> findAllTaskIds() {
return this.taskRepository.all();
}
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
new file mode 100644
index 000000000..ac2f87c16
--- /dev/null
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
@@ -0,0 +1,55 @@
+package org.sterl.spring.persistent_tasks.trigger.component;
+
+import java.time.OffsetDateTime;
+import java.util.Optional;
+
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.sterl.spring.persistent_tasks.trigger.component.RunTriggerComponent.TaskAndState;
+import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Component
+@Transactional(timeout = 30)
+@RequiredArgsConstructor
+@Slf4j
+public class HandleTriggerExceptionComponent {
+
+ private final EditTriggerComponent editTrigger;
+
+ Optional execute(TaskAndState taskAndState,
+ @Nullable Exception e) {
+
+ var trigger = taskAndState.trigger;
+ var task = taskAndState.persistentTask;
+ var result = editTrigger.completeTaskWithStatus(trigger.getKey(), taskAndState.state, e);
+
+ if (task != null
+ && task.retryStrategy().shouldRetry(trigger.getData().getExecutionCount(), e)) {
+
+ final OffsetDateTime retryAt = task.retryStrategy().retryAt(trigger.getData().getExecutionCount(), e);
+
+ result = editTrigger.retryTrigger(trigger.getKey(), retryAt);
+ if (result.isPresent()) {
+ var data = result.get().getData();
+ log.warn("{} failed, retry will be done at={} status={}!",
+ trigger.getKey(),
+ data.getRunAt(),
+ data.getStatus(),
+ e);
+ } else {
+ log.error("Trigger with key={} not found and may be at a wrong state!",
+ trigger.getKey(), e);
+ }
+ } else {
+ log.error("{} failed, no more retries! {}", trigger.getKey(),
+ e == null ? "No exception given." : e.getMessage(), e);
+
+ editTrigger.deleteTrigger(trigger);
+ }
+ return result;
+ }
+}
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 5a66453a5..6feb0fa12 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java
@@ -1,12 +1,13 @@
package org.sterl.spring.persistent_tasks.trigger.component;
import java.io.Serializable;
-import java.time.OffsetDateTime;
import java.util.Optional;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.task.TaskService;
@@ -23,12 +24,14 @@ public class RunTriggerComponent {
private final TaskService taskService;
private final EditTriggerComponent editTrigger;
+ private final HandleTriggerExceptionComponent handleTriggerException;
private final ApplicationEventPublisher eventPublisher;
private final StateSerializer serializer = new StateSerializer();
/**
* Will execute the given {@link TriggerEntity} and handle any errors etc.
*/
+ @Transactional(propagation = Propagation.NEVER)
public Optional execute(TriggerEntity trigger) {
if (trigger == null) {
return Optional.empty();
@@ -40,7 +43,7 @@ public Optional execute(TriggerEntity trigger) {
try {
return taskAndState.call();
} catch (Exception e) {
- return handleTaskException(taskAndState, e);
+ return handleTriggerException.execute(taskAndState, e);
}
}
@@ -53,12 +56,12 @@ private TaskAndState getTastAndState(TriggerEntity trigger) {
return new TaskAndState(task, trx, state, trigger);
} catch (Exception e) {
// this trigger is somehow crap, no retry and done.
- handleTaskException(new TaskAndState(null, Optional.empty(), null, trigger), e);
+ handleTriggerException.execute(new TaskAndState(null, Optional.empty(), null, trigger), e);
return null;
}
}
@RequiredArgsConstructor
- private class TaskAndState {
+ class TaskAndState {
final PersistentTask persistentTask;
final Optional trx;
final Serializable state;
@@ -73,6 +76,7 @@ Optional call() {
}
private Optional runTask() {
+ if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn());
eventPublisher.publishEvent(new TriggerRunningEvent(
trigger.getId(), trigger.copyData(), state, trigger.getRunningOn()));
@@ -84,37 +88,4 @@ private Optional runTask() {
return result;
}
}
-
- private Optional handleTaskException(TaskAndState taskAndState,
- @Nullable Exception e) {
-
- var trigger = taskAndState.trigger;
- var task = taskAndState.persistentTask;
- var result = editTrigger.completeTaskWithStatus(trigger.getKey(), taskAndState.state, e);
-
- if (task != null
- && task.retryStrategy().shouldRetry(trigger.getData().getExecutionCount(), e)) {
-
- final OffsetDateTime retryAt = task.retryStrategy().retryAt(trigger.getData().getExecutionCount(), e);
-
- result = editTrigger.retryTrigger(trigger.getKey(), retryAt);
- if (result.isPresent()) {
- var data = result.get().getData();
- log.warn("{} failed, retry will be done at={} status={}!",
- trigger.getKey(),
- data.getRunAt(),
- data.getStatus(),
- e);
- } else {
- log.error("Trigger with key={} not found and may be at a wrong state!",
- trigger.getKey(), e);
- }
- } else {
- log.error("{} failed, no more retries! {}", trigger.getKey(),
- e == null ? "No exception given." : e.getMessage(), e);
-
- editTrigger.deleteTrigger(trigger);
- }
- return result;
- }
}
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 3c39502f5..d3980aa5f 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
@@ -3,17 +3,15 @@
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.support.TransactionTemplate;
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
-import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3;
import org.sterl.spring.persistent_tasks.api.PersistentTask;
import org.sterl.spring.persistent_tasks.api.RetryStrategy;
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
@@ -22,12 +20,13 @@
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.sample_app.person.PersonBE;
import org.sterl.spring.sample_app.person.PersonRepository;
+import org.sterl.test.Countdown;
class SchedulerServiceTransactionTest extends AbstractSpringTest {
private SchedulerService subject;
private static final AtomicBoolean sendError = new AtomicBoolean(false);
- private static final AtomicInteger sleepTime = new AtomicInteger(50);
+ private static final Countdown COUNTDOWN = new Countdown();
@Autowired private PersonRepository personRepository;
@Configuration
@@ -37,12 +36,8 @@ TransactionalTask savePersonInTrx(PersonRepository personRepository) {
return new TransactionalTask() {
@Override
public void accept(String name) {
- try {
- if (sleepTime.intValue() > 0) Thread.sleep(sleepTime.intValue());
- } catch (InterruptedException e) {
- Thread.interrupted();
- }
personRepository.save(new PersonBE(name));
+ COUNTDOWN.await();
if (sendError.get()) {
throw new RuntimeException("Error requested for " + name);
}
@@ -54,14 +49,18 @@ public RetryStrategy retryStrategy() {
}
@Bean
- PersistentTask savePersonNoTrx(PersonRepository personRepository) {
+ PersistentTask savePersonNoTrx(TransactionTemplate trx,
+ PersonRepository personRepository) {
return new PersistentTask<>() {
@Override
public void accept(String name) {
- personRepository.save(new PersonBE(name));
- if (sendError.get()) {
- throw new RuntimeException("Error requested for " + name);
- }
+ trx.executeWithoutResult(t -> {
+ personRepository.save(new PersonBE(name));
+ COUNTDOWN.await();
+ if (sendError.get()) {
+ throw new RuntimeException("Error requested for " + name);
+ }
+ });
}
public RetryStrategy retryStrategy() {
return RetryStrategy.THREE_RETRIES_IMMEDIATELY;
@@ -79,43 +78,68 @@ public void beforeEach() throws Exception {
super.beforeEach();
subject = schedulerService;
personRepository.deleteAllInBatch();
+ COUNTDOWN.reset();
sendError.set(false);
}
@Test
- void testSaveTransactions() throws Exception {
+ void testSaveNoTransactions() throws Exception {
// GIVEN
final var request = TaskTriggerBuilder.newTrigger("savePersonNoTrx").state("Paul").build();
var trigger = triggerService.queue(request);
// WHEN
hibernateAsserts.reset();
- triggerService.run(trigger);
+ COUNTDOWN.countDown();
+ schedulerService.triggerNextTasks().forEach(t -> {
+ try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
+ });
// THEN
- // AND one the service, one the event and one more status update
- hibernateAsserts.assertTrxCount(4);
+ // 1. get the trigger
+ // 2. one the event running
+ // 3. for the work
+ // 4. for success status
+ hibernateAsserts.assertTrxCount(5);
assertThat(personRepository.count()).isOne();
+ // AND
+ var data = persistentTaskService.getLastDetailData(trigger.key());
+ assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.SUCCESS);
+ // AND
+ var history = historyService.findAllDetailsForKey(trigger.key()).getContent();
+ assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.SUCCESS);
+ assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING);
+ assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING);
}
-
@Test
- void testTrxCountTriggerService() throws Exception {
+ void testSaveTransactions() throws Exception {
// GIVEN
final var request = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build();
var trigger = triggerService.queue(request);
// WHEN
hibernateAsserts.reset();
- triggerService.run(trigger);
+ COUNTDOWN.countDown();
+ schedulerService.triggerNextTasks().forEach(t -> {
+ try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
+ });
// THEN
- hibernateAsserts.assertTrxCount(1);
+ hibernateAsserts.assertTrxCount(3);
assertThat(personRepository.count()).isOne();
+ // AND
+ var data = persistentTaskService.getLastDetailData(trigger.key());
+ assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.SUCCESS);
+ // AND
+ var history = historyService.findAllDetailsForKey(trigger.key()).getContent();
+ assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.SUCCESS);
+ assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING);
+ assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING);
}
@Test
- void testFailTrxCount() throws Exception {
+ void test_fail_in_transaction() throws Exception {
// GIVEN
final var request = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build();
var trigger = triggerService.queue(request);
@@ -123,17 +147,29 @@ void testFailTrxCount() throws Exception {
// WHEN
hibernateAsserts.reset();
- triggerService.run(trigger);
+ COUNTDOWN.countDown();
+ schedulerService.triggerNextTasks().forEach(t -> {
+ try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
+ });
// THEN
- // first the work which runs on error
- // second the update to the trigger
- // third to write the history
- hibernateAsserts.assertTrxCount(3);
+ // 1. Get the trigger
+ // 2. Running history
+ // 3. Run the trigger which will fail
+ // 4. Update the status to failed and write the history
+ hibernateAsserts.assertTrxCount(4);
+ // AND
+ var data = persistentTaskService.getLastDetailData(trigger.key());
+ assertThat(data.get().getStatus()).isEqualTo(TriggerStatus.FAILED);
+ // AND
+ var history = historyService.findAllDetailsForKey(trigger.key()).getContent();
+ assertThat(history.get(0).getData().getStatus()).isEqualTo(TriggerStatus.FAILED);
+ assertThat(history.get(1).getData().getStatus()).isEqualTo(TriggerStatus.RUNNING);
+ assertThat(history.get(2).getData().getStatus()).isEqualTo(TriggerStatus.WAITING);
}
@Test
- void testRunOrQueue() throws Exception {
+ void testRunOrQueueShowsRunning() throws Exception {
// GIVEN
var k1 = subject.runOrQueue(TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build());
var k2 = subject.runOrQueue(TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build());
@@ -144,27 +180,44 @@ void testRunOrQueue() throws Exception {
assertThat(persistentTaskService.getLastTriggerData(k2).get().getStatus())
.isEqualTo(TriggerStatus.RUNNING);
-
// THEN
+ Thread.sleep(150); // wait for the history async events
+ hibernateAsserts.assertTrxCount(7);
+
+ // WHEN
+ COUNTDOWN.countDown();
awaitRunningTasks();
+ // THEN
assertThat(personRepository.count()).isEqualTo(2);
+ // AND
+ assertThat(persistentTaskService.getLastTriggerData(k1).get().getStatus())
+ .isEqualTo(TriggerStatus.SUCCESS);
+ assertThat(persistentTaskService.getLastTriggerData(k2).get().getStatus())
+ .isEqualTo(TriggerStatus.SUCCESS);
}
@Test
void testRollbackAndRetry() throws Exception {
// GIVEN
- final var triggerRequest = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build();
+ final var triggerRequest = TaskTriggerBuilder.newTrigger("savePersonInTrx")
+ .state("Paul").build();
sendError.set(true);
// WHEN
var key = subject.runOrQueue(triggerRequest);
+ COUNTDOWN.countDown();
+ awaitRunningTasks();
// THEN
- awaitRunningTasks();
+ hibernateAsserts.assertInsertCount(5);
// AND the last status before we are back to running should be FAILED
- assertThat(historyService.findAllDetailsForKey(key)
- .getContent().get(0).getData().getStatus())
+ var history = historyService.findAllDetailsForKey(key).getContent();
+ assertThat(history.get(0).getData().getStatus())
.isEqualTo(TriggerStatus.FAILED);
+ assertThat(history.get(1).getData().getStatus())
+ .isEqualTo(TriggerStatus.RUNNING);
+ assertThat(history.get(2).getData().getStatus())
+ .isEqualTo(TriggerStatus.WAITING);
// WHEN
sendError.set(false);
@@ -175,26 +228,6 @@ void testRollbackAndRetry() throws Exception {
assertExecutionCount(key, 2);
assertThat(personRepository.count()).isOne();
}
-
- @Test
- void testTriggerHistoryTrx() throws TimeoutException, InterruptedException {
- // GIVEN
- sleepTime.set(0);
- final var trigger = Task3.ID.newUniqueTrigger("savePersonNoTrx");
- persistentTaskService.queue(trigger);
- // WHEN
- hibernateAsserts.reset();
- schedulerService.triggerNextTasks().forEach(t -> {
- try {t.get();} catch (Exception ex) {throw new RuntimeException(ex);}
- });
-
- // THEN
- // 2 to get the work
- // 1 for the running history
- // 1 for the success history
- hibernateAsserts.assertTrxCount(4);
- }
-
private void assertExecutionCount(TriggerKey triggerKey, int count) throws InterruptedException, ExecutionException {
var data = persistentTaskService.getLastTriggerData(triggerKey);
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 b0d34d65d..879d0896f 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
@@ -145,7 +145,7 @@ void testTransactionalTask(String task) {
triggerService.run(t).get();
// THEN
- hibernateAsserts.assertTrxCount(1);
+ hibernateAsserts.assertTrxCount(2);
assertThat(personRepository.count()).isEqualTo(2);
}
}
diff --git a/core/src/test/java/org/sterl/test/Countdown.java b/core/src/test/java/org/sterl/test/Countdown.java
new file mode 100644
index 000000000..b6bdd6830
--- /dev/null
+++ b/core/src/test/java/org/sterl/test/Countdown.java
@@ -0,0 +1,26 @@
+package org.sterl.test;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.awaitility.Awaitility;
+
+public class Countdown {
+
+ private final AtomicInteger count = new AtomicInteger(1);
+
+ public void await() {
+ Awaitility
+ .await("Countdown")
+ .atMost(Duration.ofSeconds(3))
+ .until(() -> count.get() <= 0);
+ }
+
+ public void countDown() {
+ count.decrementAndGet();
+ }
+
+ public void reset() {
+ count.set(1);
+ }
+}
From 238df1e9e14e1ae8c74308b334b357cd8e90dc00 Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Sat, 11 Jan 2025 15:54:26 +0100
Subject: [PATCH 4/9] write history only if the trigger is done
---
CHANGELOG.md | 5 ++
.../persistent_tasks/api/HistoryOverview.java | 15 -----
.../api/TaskHistoryOverview.java | 14 +++++
.../history/HistoryService.java | 5 ++
.../history/api/TriggerHistoryResource.java | 8 ++-
.../component/TriggerHistoryComponent.java | 19 ++++---
.../TriggerHistoryDetailRepository.java | 26 ++++-----
.../trigger/api/TriggerResource.java | 2 +-
.../component/EditTriggerComponent.java | 46 ++++++++++------
.../HandleTriggerExceptionComponent.java | 55 -------------------
.../component/RunTriggerComponent.java | 29 +++++++++-
.../trigger/event/TriggerAddedEvent.java | 4 ++
.../trigger/event/TriggerCanceledEvent.java | 5 ++
.../trigger/event/TriggerFailedEvent.java | 9 ++-
.../trigger/event/TriggerLifeCycleEvent.java | 4 ++
.../trigger/event/TriggerRunningEvent.java | 5 ++
.../trigger/event/TriggerSuccessEvent.java | 4 ++
.../TriggerHistoryDetailRepositoryTest.java | 11 ++--
.../SchedulerServiceTransactionTest.java | 2 -
.../trigger/TriggerServiceTest.java | 9 ++-
20 files changed, 150 insertions(+), 127 deletions(-)
delete mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryOverview.java
create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
delete mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 153e6ef0c..bf2a0b2fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## v1.5.0
+
+- Adjusted transaction handling for trigger life cycle events
+- Base event entry is only written for done/finished trigger
+
## v1.4.6 - (2025-01-08)
- Trigger history with more details - not waiting for the transaction
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryOverview.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryOverview.java
deleted file mode 100644
index 76cecc927..000000000
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/HistoryOverview.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.sterl.spring.persistent_tasks.api;
-
-import java.time.OffsetDateTime;
-
-public record HistoryOverview(
- long instanceId,
- String taskName,
- long entryCount,
- OffsetDateTime start,
- OffsetDateTime end,
- OffsetDateTime createdTime,
- long executionCount,
- double runningDurationInMs
- ) {
-}
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
new file mode 100644
index 000000000..7d0e15544
--- /dev/null
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
@@ -0,0 +1,14 @@
+package org.sterl.spring.persistent_tasks.api;
+
+import java.time.OffsetDateTime;
+
+public record TaskHistoryOverview(
+ String taskName,
+ long executionCount,
+ OffsetDateTime firstRun,
+ OffsetDateTime lastRun,
+ double maxDurationMs,
+ double minDurationMs,
+ double avgDurationMs
+ ) {
+}
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 0406cdecd..d53fa0c09 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
@@ -10,6 +10,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.lang.Nullable;
+import org.sterl.spring.persistent_tasks.api.TaskHistoryOverview;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
@@ -116,4 +117,8 @@ private Pageable applyDefaultSortIfNeeded(Pageable page) {
}
return page;
}
+
+ public List taskHistory() {
+ return triggerHistoryDetailRepository.listTaskHistoryOverview();
+ }
}
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 662d4bef9..01cf89113 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
@@ -4,7 +4,6 @@
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;
import org.springframework.data.web.PagedModel;
import org.springframework.web.bind.annotation.GetMapping;
@@ -12,6 +11,7 @@
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.TaskHistoryOverview;
import org.sterl.spring.persistent_tasks.api.Trigger;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.history.HistoryService;
@@ -32,12 +32,16 @@ public List listInstances(@PathVariable("instanceId") long instanceId)
return FromTriggerStateDetailEntity.INSTANCE.convert( //
historyService.findAllDetailsForInstance(instanceId));
}
+ @GetMapping("task-history")
+ public List taskHistory() {
+ return historyService.taskHistory();
+ }
@GetMapping("history")
public PagedModel list(
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "taskName", required = false) String taskName,
- @PageableDefault(size = 100, direction = Direction.DESC, sort = "id") Pageable pageable) {
+ @PageableDefault(size = 100) Pageable pageable) {
return FromLastTriggerStateEntity.INSTANCE.toPage( //
historyService.findTriggerState(
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 7c93d633a..14e829ec3 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
@@ -39,11 +39,9 @@ public void onRunning(TriggerRunningEvent e) {
e.getClass().getSimpleName(),
e.key(), e.status());
- execute(e.id(), e.data());
+ execute(e.id(), e.data(), false);
}
- // @Transactional(timeout = 10)
- // @EventListener
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
if (e instanceof TriggerRunningEvent) return; // we have an own listener for that
@@ -51,14 +49,17 @@ void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
e.getClass().getSimpleName(),
e.key(), e.status());
- execute(e.id(), e.data());
+
+ execute(e.id(), e.data(), e.isDone());
}
- public void execute(final long triggerId, final TriggerData data) {
- final var state = new TriggerHistoryLastStateEntity();
- state.setId(triggerId);
- state.setData(data.copy());
- triggerHistoryLastStateRepository.save(state);
+ public void execute(final long triggerId, final TriggerData data, boolean isDone) {
+ if (isDone) {
+ final var state = new TriggerHistoryLastStateEntity();
+ state.setId(triggerId);
+ state.setData(data.copy());
+ triggerHistoryLastStateRepository.save(state);
+ }
var detail = new TriggerHistoryDetailEntity();
detail.setInstanceId(triggerId);
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 a01b72da4..d5dda2af7 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
@@ -2,33 +2,29 @@
import java.util.List;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-import org.sterl.spring.persistent_tasks.api.HistoryOverview;
+import org.sterl.spring.persistent_tasks.api.TaskHistoryOverview;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository {
@Query("""
- SELECT new org.sterl.spring.persistent_tasks.api.HistoryOverview(
- e.instanceId,
+ SELECT new org.sterl.spring.persistent_tasks.api.TaskHistoryOverview(
e.data.key.taskName,
count(1) as entryCount,
- MIN(e.data.start) as start,
- MAX(e.data.end) as end,
- MIN(e.data.createdTime) as createdTime,
- MAX(e.data.executionCount) as executionCount,
- AVG(e.data.runningDurationInMs) as runningDurationInMs
+ MIN(e.data.runAt) as firstRun,
+ MAX(e.data.runAt) as lastRun,
+ MAX(e.data.runningDurationInMs) as maxDuration,
+ MIN(e.data.runningDurationInMs) as minDuration,
+ AVG(e.data.runningDurationInMs) as avgDuration
)
FROM #{#entityName} e
- GROUP BY
- e.instanceId,
- e.data.key.taskName
- ORDER BY end DESC, createdTime DESC
+ WHERE e.data.end IS NOT NULL
+ GROUP BY e.data.key.taskName
+ ORDER BY e.data.key.taskName ASC
""")
- Page listHistoryOverview(Pageable page);
+ List listTaskHistoryOverview();
@Query("""
SELECT e
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 e07681148..c6abdfd22 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
@@ -39,7 +39,7 @@ public long count() {
public PagedModel list(
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "taskName", required = false) String taskName,
- @PageableDefault(size = 100, direction = Direction.DESC, sort = "id")
+ @PageableDefault(size = 100, direction = Direction.ASC, sort = "data.runAt")
Pageable pageable) {
return FromTriggerEntity.INSTANCE.toPage(
triggerService.findAllTriggers(
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 81a1859f6..f03c9cbc5 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
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Optional;
+import org.slf4j.event.Level;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@@ -35,40 +36,51 @@ public class EditTriggerComponent {
private final TriggerRepository triggerRepository;
public Optional completeTaskWithSuccess(TriggerKey key, Serializable state) {
- return this.completeTaskWithStatus(key, state, null);
+ final Optional result = triggerRepository.findByKey(key);
+
+ result.ifPresent(t -> {
+ t.complete(null);
+ publisher.publishEvent(new TriggerSuccessEvent(
+ t.getId(), t.copyData(), state));
+ log.debug("Setting {} to status={}", key, t.getData().getStatus());
+ triggerRepository.delete(t);
+ });
+ return result;
}
/**
* Sets success or error based on the fact if an exception is given or not.
*/
- public Optional completeTaskWithStatus(TriggerKey key, Serializable state, Exception e) {
+ public Optional failTrigger(
+ TriggerKey key,
+ Serializable state,
+ Exception e,
+ OffsetDateTime retryAt) {
final Optional result = triggerRepository.findByKey(key);
+
result.ifPresent(t -> {
+ log.atLevel(retryAt == null ? Level.ERROR : Level.WARN)
+ .setCause(e)
+ .log("{} failed, retryAt={}",
+ key, retryAt == null ? "no" : retryAt);
t.complete(e);
+ publisher.publishEvent(new TriggerFailedEvent(t.getId(), t.copyData(), state, e, retryAt));
- if (t.getData().getStatus() == TriggerStatus.SUCCESS) {
- publisher.publishEvent(new TriggerSuccessEvent(
- t.getId(), t.copyData(), state));
- log.debug("Setting {} to status={} {}", key, t.getData().getStatus(),
- e == null ? "" : "error=" + e.getClass().getSimpleName());
+ if (retryAt == null) {
+ triggerRepository.delete(t);
} else {
- publisher.publishEvent(new TriggerFailedEvent(t.getId(), t.copyData(), state, e));
- log.info("Setting {} to status={} {}", key, t.getData().getStatus(),
- e == null ? "" : "error=" + e.getClass().getSimpleName());
+ t.runAt(retryAt);
}
-
});
+ if (result.isEmpty()) {
+ log.error("Trigger with key={} not found and may be at a wrong state!",
+ key, e);
+ }
return result;
}
- public Optional retryTrigger(TriggerKey id, OffsetDateTime retryAt) {
- return triggerRepository //
- .findByKey(id) //
- .map(t -> t.runAt(retryAt));
- }
-
public Optional cancelTask(TriggerKey id) {
return triggerRepository //
.findByKey(id) //
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
deleted file mode 100644
index ac2f87c16..000000000
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/HandleTriggerExceptionComponent.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.sterl.spring.persistent_tasks.trigger.component;
-
-import java.time.OffsetDateTime;
-import java.util.Optional;
-
-import org.springframework.lang.Nullable;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-import org.sterl.spring.persistent_tasks.trigger.component.RunTriggerComponent.TaskAndState;
-import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@Component
-@Transactional(timeout = 30)
-@RequiredArgsConstructor
-@Slf4j
-public class HandleTriggerExceptionComponent {
-
- private final EditTriggerComponent editTrigger;
-
- Optional execute(TaskAndState taskAndState,
- @Nullable Exception e) {
-
- var trigger = taskAndState.trigger;
- var task = taskAndState.persistentTask;
- var result = editTrigger.completeTaskWithStatus(trigger.getKey(), taskAndState.state, e);
-
- if (task != null
- && task.retryStrategy().shouldRetry(trigger.getData().getExecutionCount(), e)) {
-
- final OffsetDateTime retryAt = task.retryStrategy().retryAt(trigger.getData().getExecutionCount(), e);
-
- result = editTrigger.retryTrigger(trigger.getKey(), retryAt);
- if (result.isPresent()) {
- var data = result.get().getData();
- log.warn("{} failed, retry will be done at={} status={}!",
- trigger.getKey(),
- data.getRunAt(),
- data.getStatus(),
- e);
- } else {
- log.error("Trigger with key={} not found and may be at a wrong state!",
- trigger.getKey(), e);
- }
- } else {
- log.error("{} failed, no more retries! {}", trigger.getKey(),
- e == null ? "No exception given." : e.getMessage(), e);
-
- editTrigger.deleteTrigger(trigger);
- }
- return result;
- }
-}
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 6feb0fa12..a2d080966 100644
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java
@@ -1,6 +1,7 @@
package org.sterl.spring.persistent_tasks.trigger.component;
import java.io.Serializable;
+import java.time.OffsetDateTime;
import java.util.Optional;
import org.springframework.context.ApplicationEventPublisher;
@@ -24,7 +25,6 @@ public class RunTriggerComponent {
private final TaskService taskService;
private final EditTriggerComponent editTrigger;
- private final HandleTriggerExceptionComponent handleTriggerException;
private final ApplicationEventPublisher eventPublisher;
private final StateSerializer serializer = new StateSerializer();
@@ -43,7 +43,7 @@ public Optional execute(TriggerEntity trigger) {
try {
return taskAndState.call();
} catch (Exception e) {
- return handleTriggerException.execute(taskAndState, e);
+ return failTaskAndState(taskAndState, e);
}
}
@@ -56,10 +56,33 @@ private TaskAndState getTastAndState(TriggerEntity trigger) {
return new TaskAndState(task, trx, state, trigger);
} catch (Exception e) {
// this trigger is somehow crap, no retry and done.
- handleTriggerException.execute(new TaskAndState(null, Optional.empty(), null, trigger), e);
+ failTaskAndState(new TaskAndState(null, Optional.empty(), null, trigger), e);
return null;
}
}
+
+ private Optional failTaskAndState(TaskAndState taskAndState, Exception e) {
+
+ var trigger = taskAndState.trigger;
+ var task = taskAndState.persistentTask;
+ Optional result;
+
+ if (task != null
+ && task.retryStrategy().shouldRetry(trigger.getData().getExecutionCount(), e)) {
+
+ final OffsetDateTime retryAt = task.retryStrategy().retryAt(trigger.getData().getExecutionCount(), e);
+
+ result = editTrigger.failTrigger(trigger.getKey(), taskAndState.state, e, retryAt);
+
+ } else {
+ log.error("{} failed, no more retries! {}", trigger.getKey(),
+ e == null ? "No exception given." : e.getMessage(), e);
+
+ result = editTrigger.failTrigger(trigger.getKey(), taskAndState.state, e, null);
+ }
+ return result;
+ }
+
@RequiredArgsConstructor
class TaskAndState {
final PersistentTask persistentTask;
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 4edfd5f9d..2bcf9fc0b 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
@@ -12,4 +12,8 @@
*/
public record TriggerAddedEvent(long id, TriggerData 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/TriggerCanceledEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerCanceledEvent.java
index 36360709b..cc0e048e5 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
@@ -11,4 +11,9 @@
*
*/
public record TriggerCanceledEvent(long id, TriggerData data, Serializable state) implements TriggerLifeCycleEvent {
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
}
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 cc8d0ad34..758408d68 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
@@ -1,6 +1,7 @@
package org.sterl.spring.persistent_tasks.trigger.event;
import java.io.Serializable;
+import java.time.OffsetDateTime;
import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
@@ -9,6 +10,12 @@
* Inside a transaction, it is save to join or listen for the AFTER_COMMIT
*
*/
-public record TriggerFailedEvent(long id, TriggerData data, Serializable state, Exception exception) implements TriggerLifeCycleEvent {
+public record TriggerFailedEvent(long id,
+ TriggerData data, Serializable state,
+ Exception exception, OffsetDateTime retryAt) implements TriggerLifeCycleEvent {
+ @Override
+ public boolean isDone() {
+ return retryAt == null;
+ }
}
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 56951c19e..c5c50dc05 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
@@ -20,4 +20,8 @@ default TriggerData getData() {
TriggerData data();
@Nullable
Serializable state();
+ /**
+ * @return true if the trigger was completed, either with success, error or canceled.
+ */
+ boolean isDone();
}
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/event/TriggerRunningEvent.java
index 3fd9377fa..3b7de39f3 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
@@ -15,4 +15,9 @@ public record TriggerRunningEvent(long id, TriggerData data, Serializable state,
public boolean isRunningOn(String name) {
return isRunning() && name != null && name.equals(runningOn);
}
+
+ @Override
+ public boolean isDone() {
+ return false;
+ }
}
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 c33b35f77..fedb18b45 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
@@ -11,4 +11,8 @@
*/
public record TriggerSuccessEvent(long id, TriggerData data, Serializable state) implements TriggerLifeCycleEvent {
+ @Override
+ public boolean isDone() {
+ return true;
+ }
}
diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
index c9d3e6f6b..99e65d18c 100644
--- a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
+++ b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
@@ -6,7 +6,6 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.domain.PageRequest;
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
@@ -18,9 +17,9 @@ class TriggerHistoryDetailRepositoryTest extends AbstractSpringTest {
@Test
void testGrouping() {
// GIVEN
- var history1 = newHistoryEntry(TriggerStatus.RUNNING, OffsetDateTime.now());
+ var history1 = newHistoryEntry(TriggerStatus.FAILED, OffsetDateTime.now());
subject.save(history1);
- var history2 = newHistoryEntry(TriggerStatus.RUNNING, OffsetDateTime.now());
+ var history2 = newHistoryEntry(TriggerStatus.SUCCESS, OffsetDateTime.now());
history2.setInstanceId(history1.getInstanceId());
history2.getData().setKey(history1.getKey());
subject.save(history2);
@@ -28,10 +27,10 @@ void testGrouping() {
subject.save(newHistoryEntry(TriggerStatus.RUNNING, OffsetDateTime.now()));
// WHEN
- var result = subject.listHistoryOverview(PageRequest.of(0, 10));
+ var result = subject.listTaskHistoryOverview();
// THEN
- assertThat(result.getTotalElements()).isEqualTo(2L);
+ assertThat(result.size()).isEqualTo(2L);
}
private TriggerHistoryDetailEntity newHistoryEntry(TriggerStatus s, OffsetDateTime created) {
@@ -39,6 +38,8 @@ private TriggerHistoryDetailEntity newHistoryEntry(TriggerStatus s, OffsetDateTi
history.setId(null);
history.setCreatedTime(created);
history.getData().setStatus(s);
+ history.getData().setStart(OffsetDateTime.now());
+ history.getData().setEnd(OffsetDateTime.now().plusSeconds(10));
return history;
}
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 d3980aa5f..104f01d9b 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
@@ -209,8 +209,6 @@ void testRollbackAndRetry() throws Exception {
awaitRunningTasks();
// THEN
- hibernateAsserts.assertInsertCount(5);
- // AND the last status before we are back to running should be FAILED
var history = historyService.findAllDetailsForKey(key).getContent();
assertThat(history.get(0).getData().getStatus())
.isEqualTo(TriggerStatus.FAILED);
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 a8dc4207a..f130430ca 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
@@ -19,6 +19,7 @@
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
+import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
import org.sterl.spring.persistent_tasks.task.repository.TaskRepository;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository;
@@ -31,6 +32,8 @@ class TriggerServiceTest extends AbstractSpringTest {
private TriggerRepository triggerRepository;
@Autowired
private TaskRepository taskRepository;
+ @Autowired
+ private TriggerHistoryLastStateRepository triggerHistoryLastStateRepository;
// ensure persistentTask in the spring context
@Autowired
@@ -58,8 +61,10 @@ void testAddTrigger() throws Exception {
// THEN
hibernateAsserts.assertTrxCount(1);
- // one for the trigger and two for the history
- hibernateAsserts.assertInsertCount(3);
+ // one for the trigger and just one for the history
+ hibernateAsserts.assertInsertCount(2);
+ // AND
+ assertThat(triggerHistoryLastStateRepository.count()).isZero();
// AND
final var e = subject.get(triggerId);
assertThat(e).isPresent();
From e7ca7535cc8ccf1441947c96b349a89ccc08d115 Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Sat, 11 Jan 2025 15:54:52 +0100
Subject: [PATCH 5/9] adjusted UI that it displays running on
---
ui/src/history/history.page.tsx | 10 +-
ui/src/history/view/trigger-history.view.tsx | 7 +-
ui/src/server-api.d.ts | 21 ++-
ui/src/shared/date.util.ts | 24 +++-
ui/src/shared/view/trigger-list-item.view.tsx | 130 +++++++++---------
ui/src/trigger/triggers.page.tsx | 18 +--
6 files changed, 119 insertions(+), 91 deletions(-)
diff --git a/ui/src/history/history.page.tsx b/ui/src/history/history.page.tsx
index 7f67aadce..69402a40b 100644
--- a/ui/src/history/history.page.tsx
+++ b/ui/src/history/history.page.tsx
@@ -7,7 +7,7 @@ import ReloadButton from "@src/shared/view/reload-button.view";
import TriggerItemView from "@src/shared/view/trigger-list-item.view";
import TaskSelect from "@src/task/view/task-select.view";
import { useState } from "react";
-import { Col, Form, Row, Stack } from "react-bootstrap";
+import { Accordion, Col, Form, Row, Stack } from "react-bootstrap";
const HistoryPage = () => {
const [page, setPage] = useState(0);
@@ -63,9 +63,11 @@ const HistoryPage = () => {
/>
- {triggers.data?.content.map((t) => (
-
- ))}
+
+ {triggers.data?.content.map((t) => (
+
+ ))}
+
>
);
diff --git a/ui/src/history/view/trigger-history.view.tsx b/ui/src/history/view/trigger-history.view.tsx
index d1348c8fd..76e7e2b32 100644
--- a/ui/src/history/view/trigger-history.view.tsx
+++ b/ui/src/history/view/trigger-history.view.tsx
@@ -19,14 +19,11 @@ const TriggerHistoryListView = ({ triggers }: { triggers?: Trigger[] }) => {
-
- {" at " + formatDateTime(t.createdTime)}
-
- {formatDateTime(t.start)} - {formatDateTime(t.end)}{" "}
+ {formatDateTime(t.createdTime)}
- {t.executionCount}
+ execution: {t.executionCount}
{formatMs(t.runningDurationInMs)}
diff --git a/ui/src/server-api.d.ts b/ui/src/server-api.d.ts
index e04a567b7..20c971bc4 100644
--- a/ui/src/server-api.d.ts
+++ b/ui/src/server-api.d.ts
@@ -23,17 +23,6 @@ export interface AddTriggerRequest {
priority: number;
}
-export interface HistoryOverview {
- instanceId: number;
- taskName: string;
- entryCount: number;
- start: string;
- end: string;
- createdTime: string;
- executionCount: number;
- runningDurationInMs: number;
-}
-
export interface PersistentTask {
transactional: boolean;
}
@@ -53,6 +42,16 @@ export interface MultiplicativeRetryStrategy extends RetryStrategy {
export interface SpringBeanTask extends PersistentTask {
}
+export interface TaskHistoryOverview {
+ taskName: string;
+ executionCount: number;
+ firstRun: string;
+ lastRun: string;
+ maxDurationMs: number;
+ minDurationMs: number;
+ avgDurationMs: number;
+}
+
export interface TaskId extends Serializable {
name: string;
}
diff --git a/ui/src/shared/date.util.ts b/ui/src/shared/date.util.ts
index 3d6addfed..86ee73c0f 100644
--- a/ui/src/shared/date.util.ts
+++ b/ui/src/shared/date.util.ts
@@ -1,9 +1,14 @@
-export function formatDateTime(inputDate?: string | Date): string {
+export function formatShortDateTime(inputDate?: string | Date): string {
if (!inputDate) return "";
const date = inputDate instanceof Date ? inputDate : new Date(inputDate);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
+
+ const secondsPast = Math.floor(now.getTime() - date.getTime() / 1000);
+ if (secondsPast > 0 && secondsPast < 6) return "just now";
+ if (secondsPast > 0 && secondsPast < 30) return secondsPast + "s";
+
const options = {
hour: "2-digit",
minute: "2-digit",
@@ -22,6 +27,23 @@ export function formatDateTime(inputDate?: string | Date): string {
).format(date);
}
+export function formatDateTime(inputDate?: string | Date): string {
+ if (!inputDate) return "";
+ const date = inputDate instanceof Date ? inputDate : new Date(inputDate);
+ return new Intl.DateTimeFormat(
+ navigator.language || "en-US",
+ {
+ day: "2-digit",
+ month: "2-digit",
+ year: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false, // Use 24-hour format
+ }
+ ).format(date);
+}
+
export function formatMs(ms?: number) {
if (ms === 0) return "0ms";
if (!ms) return "";
diff --git a/ui/src/shared/view/trigger-list-item.view.tsx b/ui/src/shared/view/trigger-list-item.view.tsx
index 2416278d6..c034dd8da 100644
--- a/ui/src/shared/view/trigger-list-item.view.tsx
+++ b/ui/src/shared/view/trigger-list-item.view.tsx
@@ -4,7 +4,7 @@ import LabeledText from "@src/shared/view/labled-text.view";
import JsonView from "@uiw/react-json-view";
import { Accordion, Button, Col, Container, Row } from "react-bootstrap";
import TriggerStatusView from "../../trigger/views/trigger-staus.view";
-import { formatDateTime, formatMs } from "../date.util";
+import { formatMs, formatShortDateTime } from "../date.util";
import { useServerObject } from "../http-request";
import HttpErrorView from "./http-error.view";
import StackTraceView from "./stacktrace-view";
@@ -28,57 +28,56 @@ const TriggerItemView = ({ trigger, afterTriggerChanged }: TriggerProps) => {
);
return (
- {
if (!triggerHistory.data) triggerHistory.doGet();
}}
>
-
-
-
-
-
-
-
-
- {trigger.status === "WAITING" && afterTriggerChanged ? (
-
-
-
-
- ) : undefined}
-
+
+
-
-
-
+
+
+
+
+ {trigger.status === "WAITING" && afterTriggerChanged ? (
+
+
+
+
+ ) : undefined}
+
+
+
);
};
@@ -95,10 +94,23 @@ const TriggerCompactView = ({ trigger }: { trigger: Trigger }) => (
{" " + trigger.key.taskName}
-
+
-
-
+
+ {trigger.runningOn ? (
+
+ ) : (
+
+ )}
);
@@ -113,19 +125,13 @@ const TriggerDetailsView = ({
return (
<>
-
-
-
-
+
-
-
+
+
-
+
@@ -133,19 +139,19 @@ const TriggerDetailsView = ({
diff --git a/ui/src/trigger/triggers.page.tsx b/ui/src/trigger/triggers.page.tsx
index e81dcca57..42343937a 100644
--- a/ui/src/trigger/triggers.page.tsx
+++ b/ui/src/trigger/triggers.page.tsx
@@ -6,7 +6,7 @@ import PageView from "@src/shared/view/page.view";
import ReloadButton from "@src/shared/view/reload-button.view";
import TaskSelect from "@src/task/view/task-select.view";
import { useState } from "react";
-import { Col, Form, Row, Stack } from "react-bootstrap";
+import { Accordion, Col, Form, Row, Stack } from "react-bootstrap";
import TriggerItemView from "../shared/view/trigger-list-item.view";
const TriggersPage = () => {
@@ -56,13 +56,15 @@ const TriggersPage = () => {
/>
- {triggers.data?.content.map((t) => (
-
- ))}
+
+ {triggers.data?.content.map((t) => (
+
+ ))}
+
);
};
From 2629e2ffe831eca3541d54e55faab73329b5111c Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Sat, 11 Jan 2025 18:46:27 +0100
Subject: [PATCH 6/9] showing trigger stats
---
.../api/TaskHistoryOverview.java | 14 ----
.../api/TaskStatusHistoryOverview.java | 16 ++++
.../history/HistoryService.java | 6 +-
.../history/api/TriggerHistoryResource.java | 8 +-
.../TriggerHistoryDetailRepository.java | 18 -----
.../TriggerHistoryLastStateRepository.java | 21 ++++++
.../TriggerHistoryDetailRepositoryTest.java | 46 ------------
...TriggerHistoryLastStateRepositoryTest.java | 75 +++++++++++++++++++
ui/src/scheduler/scheduler.page.tsx | 66 ++++++++++++++--
ui/src/server-api.d.ts | 18 +++--
ui/src/shared/date.util.ts | 5 +-
ui/src/shared/view/trigger-list-item.view.tsx | 4 +-
ui/src/task/view/staus.view.tsx | 21 ++++++
13 files changed, 215 insertions(+), 103 deletions(-)
delete mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskStatusHistoryOverview.java
delete mode 100644 core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
create mode 100644 core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepositoryTest.java
create mode 100644 ui/src/task/view/staus.view.tsx
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
deleted file mode 100644
index 7d0e15544..000000000
--- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskHistoryOverview.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.sterl.spring.persistent_tasks.api;
-
-import java.time.OffsetDateTime;
-
-public record TaskHistoryOverview(
- String taskName,
- long executionCount,
- OffsetDateTime firstRun,
- OffsetDateTime lastRun,
- double maxDurationMs,
- double minDurationMs,
- double avgDurationMs
- ) {
-}
diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskStatusHistoryOverview.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskStatusHistoryOverview.java
new file mode 100644
index 000000000..5619d4a50
--- /dev/null
+++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskStatusHistoryOverview.java
@@ -0,0 +1,16 @@
+package org.sterl.spring.persistent_tasks.api;
+
+import java.time.OffsetDateTime;
+
+public record TaskStatusHistoryOverview(
+ String taskName,
+ TriggerStatus status,
+ Long executionCount,
+ OffsetDateTime firstRun,
+ OffsetDateTime lastRun,
+ Number maxDurationMs,
+ Number minDurationMs,
+ Number avgDurationMs,
+ Number avgExecutionCount
+ ) {
+}
\ No newline at end of file
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 d53fa0c09..92012085a 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
@@ -10,7 +10,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.lang.Nullable;
-import org.sterl.spring.persistent_tasks.api.TaskHistoryOverview;
+import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
@@ -118,7 +118,7 @@ private Pageable applyDefaultSortIfNeeded(Pageable page) {
return page;
}
- public List taskHistory() {
- return triggerHistoryDetailRepository.listTaskHistoryOverview();
+ public List taskStatusHistory() {
+ return triggerHistoryLastStateRepository.listTriggerStatus();
}
}
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 01cf89113..eb2ec1f18 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
@@ -11,7 +11,7 @@
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.TaskHistoryOverview;
+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.history.HistoryService;
@@ -32,9 +32,9 @@ public List listInstances(@PathVariable("instanceId") long instanceId)
return FromTriggerStateDetailEntity.INSTANCE.convert( //
historyService.findAllDetailsForInstance(instanceId));
}
- @GetMapping("task-history")
- public List taskHistory() {
- return historyService.taskHistory();
+ @GetMapping("task-status-history")
+ public List taskStatusHistory() {
+ return historyService.taskStatusHistory();
}
@GetMapping("history")
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 d5dda2af7..0f2f42ab7 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,28 +4,10 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-import org.sterl.spring.persistent_tasks.api.TaskHistoryOverview;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
public interface TriggerHistoryDetailRepository extends HistoryTriggerRepository {
- @Query("""
- SELECT new org.sterl.spring.persistent_tasks.api.TaskHistoryOverview(
- e.data.key.taskName,
- count(1) as entryCount,
- MIN(e.data.runAt) as firstRun,
- MAX(e.data.runAt) as lastRun,
- MAX(e.data.runningDurationInMs) as maxDuration,
- MIN(e.data.runningDurationInMs) as minDuration,
- AVG(e.data.runningDurationInMs) as avgDuration
- )
- FROM #{#entityName} e
- WHERE e.data.end IS NOT NULL
- GROUP BY e.data.key.taskName
- ORDER BY e.data.key.taskName ASC
- """)
- List listTaskHistoryOverview();
-
@Query("""
SELECT e
FROM #{#entityName} e
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/TriggerHistoryLastStateRepository.java
index 5bc208d9c..0f0afcf25 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/TriggerHistoryLastStateRepository.java
@@ -1,7 +1,28 @@
package org.sterl.spring.persistent_tasks.history.repository;
+import java.util.List;
+
+import org.springframework.data.jpa.repository.Query;
+import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
public interface TriggerHistoryLastStateRepository extends HistoryTriggerRepository {
+ @Query("""
+ SELECT new org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview(
+ e.data.key.taskName,
+ e.data.status,
+ count(1),
+ MIN(e.data.runAt) as firstRun,
+ MAX(e.data.runAt) as lastRun,
+ MAX(e.data.runningDurationInMs) as maxDuration,
+ MIN(e.data.runningDurationInMs) as minDuration,
+ AVG(e.data.runningDurationInMs) as avgDuration,
+ AVG(e.data.executionCount) as avgExecutionCount
+ )
+ FROM #{#entityName} e
+ GROUP BY e.data.key.taskName, e.data.status
+ ORDER BY e.data.key.taskName ASC, e.data.status ASC
+ """)
+ List listTriggerStatus();
}
diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
deleted file mode 100644
index 99e65d18c..000000000
--- a/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryDetailRepositoryTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.sterl.spring.persistent_tasks.history.repository;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.time.OffsetDateTime;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.sterl.spring.persistent_tasks.AbstractSpringTest;
-import org.sterl.spring.persistent_tasks.api.TriggerStatus;
-import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
-
-class TriggerHistoryDetailRepositoryTest extends AbstractSpringTest {
-
- @Autowired private TriggerHistoryDetailRepository subject;
-
- @Test
- void testGrouping() {
- // GIVEN
- var history1 = newHistoryEntry(TriggerStatus.FAILED, OffsetDateTime.now());
- subject.save(history1);
- var history2 = newHistoryEntry(TriggerStatus.SUCCESS, OffsetDateTime.now());
- history2.setInstanceId(history1.getInstanceId());
- history2.getData().setKey(history1.getKey());
- subject.save(history2);
- // AND
- subject.save(newHistoryEntry(TriggerStatus.RUNNING, OffsetDateTime.now()));
-
- // WHEN
- var result = subject.listTaskHistoryOverview();
-
- // THEN
- assertThat(result.size()).isEqualTo(2L);
- }
-
- private TriggerHistoryDetailEntity newHistoryEntry(TriggerStatus s, OffsetDateTime created) {
- var history = pm.manufacturePojo(TriggerHistoryDetailEntity.class);
- history.setId(null);
- history.setCreatedTime(created);
- history.getData().setStatus(s);
- history.getData().setStart(OffsetDateTime.now());
- history.getData().setEnd(OffsetDateTime.now().plusSeconds(10));
- return history;
- }
-
-}
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/TriggerHistoryLastStateRepositoryTest.java
new file mode 100644
index 000000000..d52dd0c30
--- /dev/null
+++ b/core/src/test/java/org/sterl/spring/persistent_tasks/history/repository/TriggerHistoryLastStateRepositoryTest.java
@@ -0,0 +1,75 @@
+package org.sterl.spring.persistent_tasks.history.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.OffsetDateTime;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+class TriggerHistoryLastStateRepositoryTest extends AbstractSpringTest {
+
+ final AtomicLong idGenerator = new AtomicLong(0);
+ @Autowired
+ private TriggerHistoryLastStateRepository subject;
+
+ @Test
+ void testListTriggerStatus() {
+ // GIVEN
+ subject.deleteAllInBatch();
+ createStatus(new TriggerKey("1", "task1"), TriggerStatus.SUCCESS);
+ createStatus(new TriggerKey("2", "task1"), TriggerStatus.SUCCESS);
+ createStatus(new TriggerKey("3", "task1"), TriggerStatus.FAILED);
+ createStatus(new TriggerKey("4", "task2"), TriggerStatus.SUCCESS);
+ createStatus(new TriggerKey("5", "task2"), TriggerStatus.CANCELED);
+ assertThat(subject.count()).isEqualTo(5);
+
+ // THEN
+ var result = subject.listTriggerStatus();
+
+ // WHEN
+ assertThat(result.size()).isEqualTo(4);
+ // AND
+ var i = 0;
+ assertThat(result.get(i).taskName()).isEqualTo("task1");
+ assertThat(result.get(i).status()).isEqualTo(TriggerStatus.FAILED);
+ assertThat(result.get(i).executionCount()).isEqualTo(1L);
+ // AND
+ i = 1;
+ assertThat(result.get(i).taskName()).isEqualTo("task1");
+ assertThat(result.get(i).status()).isEqualTo(TriggerStatus.SUCCESS);
+ assertThat(result.get(i).executionCount()).isEqualTo(2L);
+ // AND
+ i = 2;
+ assertThat(result.get(i).taskName()).isEqualTo("task2");
+ assertThat(result.get(i).status()).isEqualTo(TriggerStatus.CANCELED);
+ assertThat(result.get(i).executionCount()).isEqualTo(1L);
+ }
+
+ private TriggerHistoryLastStateEntity createStatus(TriggerKey key, TriggerStatus status) {
+ final var now = OffsetDateTime.now();
+ final var isCancel = status == TriggerStatus.CANCELED;
+
+ TriggerHistoryLastStateEntity result = new TriggerHistoryLastStateEntity();
+ result.setId(idGenerator.incrementAndGet());
+ result.setData(TriggerData
+ .builder()
+ .start(isCancel ? null : now.minusMinutes(1))
+ .end(isCancel ? null : now)
+ .createdTime(now)
+ .key(key)
+ .status(status)
+ .runningDurationInMs(isCancel ? null : 600L)
+ .build()
+ );
+
+ return subject.save(result);
+ }
+
+}
diff --git a/ui/src/scheduler/scheduler.page.tsx b/ui/src/scheduler/scheduler.page.tsx
index d91c1f46a..e4a89eb18 100644
--- a/ui/src/scheduler/scheduler.page.tsx
+++ b/ui/src/scheduler/scheduler.page.tsx
@@ -1,18 +1,25 @@
import SchedulerStatusView from "@src/scheduler/views/scheduler.view";
-import HttpErrorView from "@src/shared/view/http-error.view";
+import { TaskStatusHistoryOverview } from "@src/server-api";
+import { formatMs } from "@src/shared/date.util";
import { useServerObject } from "@src/shared/http-request";
import useAutoRefresh from "@src/shared/use-auto-refresh";
+import HttpErrorView from "@src/shared/view/http-error.view";
+import StatusView from "@src/task/view/staus.view";
import { useEffect } from "react";
-import { Col, Row } from "react-bootstrap";
+import { Card, Col, ListGroup, Row } from "react-bootstrap";
const SchedulersPage = () => {
const schedulers = useServerObject(
"/spring-tasks-api/schedulers"
);
const tasks = useServerObject("/spring-tasks-api/tasks");
+ const taskHistory = useServerObject(
+ "/spring-tasks-api/task-status-history"
+ );
- useEffect(tasks.doGet, []);
- useAutoRefresh(10000, schedulers.doGet, []);
+ useEffect(() => tasks.doGet(), []);
+ useAutoRefresh(10000, () => schedulers.doGet(), []);
+ useAutoRefresh(10000, () => taskHistory.doGet(), []);
return (
<>
@@ -27,9 +34,58 @@ const SchedulersPage = () => {
))}
+
+ {tasks.data?.map((i) => (
+
+
+
+ ))}
>
);
};
-
export default SchedulersPage;
+
+const TaskStatusHistoryOverviewView = ({
+ name,
+ status,
+}: {
+ name: string;
+ status: TaskStatusHistoryOverview[];
+}) => (
+
+
+ {name}
+
+
+ {status
+ .filter((s) => s.taskName == name)
+ .map((s) => (
+
+
+
+
+
+ avg: {formatMs(s.avgDurationMs)}
+ max: {formatMs(s.maxDurationMs)}
+
+ avg retry:{" "}
+ {Math.round(
+ Math.max(0, s.avgExecutionCount - 1) * 100
+ ) / 100}
+
+
+
+ ))}
+
+
+);
diff --git a/ui/src/server-api.d.ts b/ui/src/server-api.d.ts
index 20c971bc4..5404fa63f 100644
--- a/ui/src/server-api.d.ts
+++ b/ui/src/server-api.d.ts
@@ -42,21 +42,23 @@ export interface MultiplicativeRetryStrategy extends RetryStrategy {
export interface SpringBeanTask extends PersistentTask {
}
-export interface TaskHistoryOverview {
+export interface TaskId extends Serializable {
+ name: string;
+}
+
+export interface TaskTriggerBuilder {
+}
+
+export interface TaskStatusHistoryOverview {
taskName: string;
+ status: TriggerStatus;
executionCount: number;
firstRun: string;
lastRun: string;
maxDurationMs: number;
minDurationMs: number;
avgDurationMs: number;
-}
-
-export interface TaskId extends Serializable {
- name: string;
-}
-
-export interface TaskTriggerBuilder {
+ avgExecutionCount: number;
}
export interface TransactionalTask extends PersistentTask {
diff --git a/ui/src/shared/date.util.ts b/ui/src/shared/date.util.ts
index 86ee73c0f..db80d9cda 100644
--- a/ui/src/shared/date.util.ts
+++ b/ui/src/shared/date.util.ts
@@ -45,9 +45,10 @@ export function formatDateTime(inputDate?: string | Date): string {
}
export function formatMs(ms?: number) {
+ if (ms === undefined || ms === null) return "-";
if (ms === 0) return "0ms";
- if (!ms) return "";
- if (ms < 9999) return ms + "ms";
+
+ if (ms < 9999) return Math.floor(ms) + "ms";
const inS = Math.floor(ms / 1000);
if (ms < 99999) {
diff --git a/ui/src/shared/view/trigger-list-item.view.tsx b/ui/src/shared/view/trigger-list-item.view.tsx
index c034dd8da..30b9c6308 100644
--- a/ui/src/shared/view/trigger-list-item.view.tsx
+++ b/ui/src/shared/view/trigger-list-item.view.tsx
@@ -30,9 +30,7 @@ const TriggerItemView = ({ trigger, afterTriggerChanged }: TriggerProps) => {
return (
{
- if (!triggerHistory.data) triggerHistory.doGet();
- }}
+ onClick={() => triggerHistory.doGet()}
>
diff --git a/ui/src/task/view/staus.view.tsx b/ui/src/task/view/staus.view.tsx
new file mode 100644
index 000000000..c2134d017
--- /dev/null
+++ b/ui/src/task/view/staus.view.tsx
@@ -0,0 +1,21 @@
+import { TriggerStatus } from "@src/server-api";
+import { Badge } from "react-bootstrap";
+
+interface Props {
+ status: TriggerStatus;
+ suffix?: string;
+}
+const StatusView = ({ status, suffix }: Props) => {
+ if (status === "SUCCESS")
+ return Success{suffix ?? ""};
+ if (status === "FAILED")
+ return Failed{suffix ?? ""};
+ if (status === "RUNNING") return Running{suffix ?? ""};
+ if (status === "WAITING")
+ return Wating{suffix ?? ""};
+ if (status === "CANCELED")
+ return Canceled{suffix ?? ""};
+ return {status};
+};
+
+export default StatusView;
From e7f51540b1e4f0c072adee10e949db63769988a5 Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Sat, 11 Jan 2025 18:46:39 +0100
Subject: [PATCH 7/9] failing task may succeed
---
.../example_app/vehicle/task/BuildVehicleTask.java | 10 ++++++----
.../vehicle/task/FailingBuildVehicleTask.java | 13 ++++++++-----
2 files changed, 14 insertions(+), 9 deletions(-)
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 6a263979a..adc3d998f 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
@@ -1,11 +1,13 @@
package org.sterl.spring.example_app.vehicle.task;
+import java.util.Random;
+
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.sterl.spring.example_app.vehicle.model.Vehicle;
import org.sterl.spring.example_app.vehicle.repository.VehicleRepository;
-import org.sterl.spring.persistent_tasks.api.SpringBeanTask;
import org.sterl.spring.persistent_tasks.api.TaskId;
+import org.sterl.spring.persistent_tasks.api.TransactionalTask;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -13,11 +15,11 @@
@Component(BuildVehicleTask.NAME)
@RequiredArgsConstructor
@Slf4j
-public class BuildVehicleTask implements SpringBeanTask {
+public class BuildVehicleTask implements TransactionalTask {
static final String NAME = "buildVehicleTask";
public static final TaskId ID = new TaskId<>(NAME);
-
+ private final Random random = new Random();
private final VehicleRepository vehicleRepository;
@Transactional(timeout = 5)
@@ -26,7 +28,7 @@ public void accept(Vehicle vehicle) {
vehicleRepository.save(vehicle);
log.info("Create vehicle ={}", vehicle);
try {
- Thread.sleep(1500);
+ Thread.sleep(random.nextInt(3501));
} catch (InterruptedException e) {
Thread.interrupted();
}
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 45c89aba3..f0c1eac8c 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
@@ -1,10 +1,12 @@
package org.sterl.spring.example_app.vehicle.task;
+import java.util.Random;
+
import org.springframework.stereotype.Component;
import org.sterl.spring.example_app.vehicle.model.Vehicle;
import org.sterl.spring.example_app.vehicle.repository.VehicleRepository;
-import org.sterl.spring.persistent_tasks.api.SpringBeanTask;
import org.sterl.spring.persistent_tasks.api.TaskId;
+import org.sterl.spring.persistent_tasks.api.TransactionalTask;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -12,11 +14,11 @@
@Component(FailingBuildVehicleTask.NAME)
@RequiredArgsConstructor
@Slf4j
-public class FailingBuildVehicleTask implements SpringBeanTask {
+public class FailingBuildVehicleTask implements TransactionalTask {
static final String NAME = "failingBuildVehicleTask";
public static final TaskId ID = new TaskId<>(NAME);
-
+ private final Random random = new Random();
private final VehicleRepository vehicleRepository;
@Override
@@ -24,10 +26,11 @@ public void accept(Vehicle vehicle) {
vehicleRepository.save(vehicle);
log.info("Create vehicle with {} - which will fail", vehicle);
try {
- Thread.sleep(3500);
+ Thread.sleep(random.nextInt(3501));
} catch (InterruptedException e) {
Thread.interrupted();
}
- throw new RuntimeException("This persistentTask will always fail!");
+ if (random.nextInt(11) % 2 == 0)
+ throw new RuntimeException("This persistentTask will always fail!");
}
}
From 8b2efbfc2570ee17c0d01a584f03b44dd57a5430 Mon Sep 17 00:00:00 2001
From: Paul Sterl
Date: Sat, 11 Jan 2025 18:49:35 +0100
Subject: [PATCH 8/9] update changelog
---
CHANGELOG.md | 34 +++++++++++++++---------------
RUN_AND_BUILD.md | 10 ++++++---
screenshots/history-screen.png | Bin 393095 -> 256731 bytes
screenshots/schedulers-screen.png | Bin 93422 -> 173213 bytes
4 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf2a0b2fc..60ca2009a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,47 +2,47 @@
## v1.5.0
-- Adjusted transaction handling for trigger life cycle events
-- Base event entry is only written for done/finished trigger
+- Adjusted transaction handling for trigger life cycle events
+- Base event entry is only written for done/finished trigger
+- Base statistics added for a task
## v1.4.6 - (2025-01-08)
-- Trigger history with more details - not waiting for the transaction
+- Trigger history with more details - not waiting for the transaction
## v1.4.5 - (2025-01-08)
-- Adjusted path matching to support sub routes for an SPA web app
+- Adjusted path matching to support sub routes for an SPA web app
## v1.4.4 - (2025-01-08)
-- Fixed UI routing
-- added support for thymeleaf - adding index.html to template folder
+- Fixed UI routing
+- added support for thymeleaf - adding index.html to template folder
## v1.4.3 - (2025-01-08)
-- Scheduler service leaves current transaction before executing task
+- Scheduler service leaves current transaction before executing task
## v1.4.2 - (2025-01-06)
-- Fixed count by TaskId
-- added search by ID to the UI
-- added search by task to history
+- Fixed count by TaskId
+- added search by ID to the UI
+- added search by task to history
## v1.4.1 - (2025-01-06)
-- Added state to the TriggerLifeCycleEvent
-- @Transactional annotation is taken from the method first
+- Added state to the TriggerLifeCycleEvent
+- @Transactional annotation is taken from the method first
## v1.4.0 - (2025-01-05)
-- @Transactional Annotation support
-- PersistentTask instead of Task or SpringBeanTask
-
+- @Transactional Annotation support
+- PersistentTask instead of Task or SpringBeanTask
## v1.3.1 - (2025-01-02)
-- Bugfixes
-- Sprign Transaction Template support
+- Bugfixes
+- Sprign Transaction Template support
## v1.3.0 - (2025-01-01)
diff --git a/RUN_AND_BUILD.md b/RUN_AND_BUILD.md
index 3d589d795..3a188efa8 100644
--- a/RUN_AND_BUILD.md
+++ b/RUN_AND_BUILD.md
@@ -1,16 +1,20 @@
mvn versions:display-dependency-updates
-mvn versions:set -DnewVersion=1.4.6 -DgenerateBackupPoms=false
-git tag -a v1.4.6 -m "v1.4.6 release"
-mvn versions:set -DnewVersion=1.4.7-SNAPSHOT -DgenerateBackupPoms=false
+mvn versions:set -DnewVersion=1.5.0 -DgenerateBackupPoms=false
+git tag -a v1.5.0 -m "v1.5.0 release"
+mvn versions:set -DnewVersion=1.5.1-SNAPSHOT -DgenerateBackupPoms=false
## postgres
+
docker run --name pg-container -e POSTGRES_USER=sa -e POSTGRES_PASSWORD=veryStrong123 -p 5432:5432 -d postgres
## azure-sql-edge
+
docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=veryStrong123' -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge
## MariaDB
+
docker run -e MYSQL_ROOT_PASSWORD=veryStrong123 -e MYSQL_DATABASE=testdb -e MYSQL_USER=sa -e MYSQL_PASSWORD=veryStrong123 -p 3306:3306 -d mariadb:latest
## MySQL
+
docker run -e MYSQL_ROOT_PASSWORD=veryStrong123 -e MYSQL_DATABASE=testdb -e MYSQL_USER=sa -e MYSQL_PASSWORD=veryStrong123 -p 3306:3306 -d mysql
diff --git a/screenshots/history-screen.png b/screenshots/history-screen.png
index 094d123d8787656e3120e1ed8b27349e3a42aa20..4952a6f8ecae6d8282532ad27f8f80f0701f21e0 100644
GIT binary patch
literal 256731
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