From 5c6e650c5a93b84e36bfc26d405852016230def0 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:29:00 +0100 Subject: [PATCH 01/10] added interceptor to write task name and id to MDC logging context --- CHANGELOG.md | 4 ++ .../persistent_tasks/api/TriggerKey.java | 4 +- .../SpringPersistentTaskException.java | 19 ++++++ .../component/EditTriggerComponent.java | 9 ++- .../component/RunTriggerComponent.java | 62 +++++-------------- .../trigger/component/StateSerializer.java | 21 ++++++- .../interceptor/MdcTriggerInterceptor.java | 34 ++++++++++ .../model/RunTaskWithStateCommand.java | 41 ++++++++++++ .../persistent_tasks/AbstractSpringTest.java | 2 + .../trigger/TriggerServiceTest.java | 29 ++++++++- 10 files changed, 171 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/exception/SpringPersistentTaskException.java create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/MdcTriggerInterceptor.java create mode 100644 core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ffdde1ce..3135df6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.5.5 + +- Added MdcTriggerInterceptor + ## v1.5.4 - (2025-01-14) - adjusted trigger cols that the UI does not break diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java index 53b45bf42..6fe4e83c1 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java @@ -10,6 +10,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.ToString; /** * Unique key of a trigger during it's execution. But it after that the same key @@ -17,7 +18,8 @@ * is currently scheduled for execution. */ @Data -@Builder(toBuilder = true) +@ToString +@Builder @NoArgsConstructor @AllArgsConstructor public class TriggerKey implements Serializable { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/exception/SpringPersistentTaskException.java b/core/src/main/java/org/sterl/spring/persistent_tasks/exception/SpringPersistentTaskException.java new file mode 100644 index 000000000..3469aa430 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/exception/SpringPersistentTaskException.java @@ -0,0 +1,19 @@ +package org.sterl.spring.persistent_tasks.exception; + +import lombok.Getter; + +public class SpringPersistentTaskException extends RuntimeException { + private static final long serialVersionUID = 1L; + @Getter + protected final Object state; + + public SpringPersistentTaskException(String message, Object state, Throwable cause) { + super(message, cause); + this.state = state; + } + + public SpringPersistentTaskException(String message, Object state) { + super(message); + this.state = state; + } +} 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 f03c9cbc5..b0363f403 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 @@ -18,6 +18,7 @@ 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; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent; import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; @@ -40,9 +41,9 @@ public Optional completeTaskWithSuccess(TriggerKey key, Serializa result.ifPresent(t -> { t.complete(null); + log.debug("{} set to status={}", key, t.getData().getStatus()); publisher.publishEvent(new TriggerSuccessEvent( t.getId(), t.copyData(), state)); - log.debug("Setting {} to status={}", key, t.getData().getStatus()); triggerRepository.delete(t); }); return result; @@ -146,4 +147,10 @@ public int markTriggersAsRunning(Collection keys, String runOn) { return triggerRepository.markTriggersAsRunning(keys, runOn, OffsetDateTime.now(), TriggerStatus.RUNNING); } + + public void triggerIsNowRunning(TriggerEntity trigger, Serializable state) { + if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn()); + publisher.publishEvent(new TriggerRunningEvent( + trigger.getId(), trigger.copyData(), state, trigger.getRunningOn())); + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/RunTriggerComponent.java index a2d080966..f09f76335 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,18 +1,14 @@ 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; -import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent; +import org.sterl.spring.persistent_tasks.trigger.model.RunTaskWithStateCommand; import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; import lombok.RequiredArgsConstructor; @@ -25,7 +21,6 @@ public class RunTriggerComponent { private final TaskService taskService; private final EditTriggerComponent editTrigger; - private final ApplicationEventPublisher eventPublisher; private final StateSerializer serializer = new StateSerializer(); /** @@ -36,35 +31,35 @@ public Optional execute(TriggerEntity trigger) { if (trigger == null) { return Optional.empty(); } + final var taskAndState = getTastAndState(trigger); // something went really wrong this trigger is crap if (taskAndState == null) return Optional.of(trigger); try { - return taskAndState.call(); + return taskAndState.execute(editTrigger); } catch (Exception e) { return failTaskAndState(taskAndState, e); } } @Nullable - private TaskAndState getTastAndState(TriggerEntity trigger) { + private RunTaskWithStateCommand getTastAndState(TriggerEntity trigger) { try { - var task = taskService.assertIsKnown(trigger.newTaskId()); - var trx = taskService.getTransactionTemplate(task); - var state = serializer.deserialize(trigger.getData().getState()); - return new TaskAndState(task, trx, state, trigger); + final var task = taskService.assertIsKnown(trigger.newTaskId()); + final var trx = taskService.getTransactionTemplate(task); + final var state = serializer.deserialize(trigger.getData().getState()); + return new RunTaskWithStateCommand(task, trx, state, trigger); } catch (Exception e) { - // this trigger is somehow crap, no retry and done. - failTaskAndState(new TaskAndState(null, Optional.empty(), null, trigger), e); + failTaskAndState(new RunTaskWithStateCommand(null, Optional.empty(), null, trigger), e); return null; } } - private Optional failTaskAndState(TaskAndState taskAndState, Exception e) { + private Optional failTaskAndState(RunTaskWithStateCommand runTaskWithStateCommand, Exception e) { - var trigger = taskAndState.trigger; - var task = taskAndState.persistentTask; + var trigger = runTaskWithStateCommand.trigger(); + var task = runTaskWithStateCommand.task(); Optional result; if (task != null @@ -72,43 +67,14 @@ private Optional failTaskAndState(TaskAndState taskAndState, Exce final OffsetDateTime retryAt = task.retryStrategy().retryAt(trigger.getData().getExecutionCount(), e); - result = editTrigger.failTrigger(trigger.getKey(), taskAndState.state, e, retryAt); + result = editTrigger.failTrigger(trigger.getKey(), runTaskWithStateCommand.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); + result = editTrigger.failTrigger(trigger.getKey(), runTaskWithStateCommand.state(), e, null); } return result; } - - @RequiredArgsConstructor - class TaskAndState { - final PersistentTask persistentTask; - final Optional trx; - final Serializable state; - final TriggerEntity trigger; - - Optional call() { - if (trx.isPresent()) { - return trx.get().execute(t -> runTask()); - } else { - return runTask(); - } - } - - private Optional runTask() { - if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn()); - eventPublisher.publishEvent(new TriggerRunningEvent( - trigger.getId(), trigger.copyData(), state, trigger.getRunningOn())); - - persistentTask.accept(state); - - var result = editTrigger.completeTaskWithSuccess(trigger.getKey(), state); - editTrigger.deleteTrigger(trigger); - - return result; - } - } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializer.java index ad1d61e92..410d70986 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializer.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializer.java @@ -7,10 +7,27 @@ import java.io.ObjectOutputStream; import java.io.Serializable; +import org.sterl.spring.persistent_tasks.exception.SpringPersistentTaskException; + import lombok.extern.slf4j.Slf4j; @Slf4j public class StateSerializer { + public static class DeSerializationFailedException extends SpringPersistentTaskException { + private static final long serialVersionUID = 1L; + + public DeSerializationFailedException(byte[] bytes, Exception e) { + super("Failed to deserialize state of length " + bytes.length, bytes, e); + } + } + + public static class SerializationFailedException extends SpringPersistentTaskException { + private static final long serialVersionUID = 1L; + + public SerializationFailedException(Serializable obj, Exception e) { + super("Failed to serialize state " + obj.getClass(), obj, e); + } + } public byte[] serialize(final Serializable obj) { if (obj == null) { @@ -23,7 +40,7 @@ public byte[] serialize(final Serializable obj) { out.writeObject(obj); return bos.toByteArray(); } catch (Exception ex) { - throw new RuntimeException(ex); + throw new SerializationFailedException(obj, ex); } } @@ -36,7 +53,7 @@ public Serializable deserialize(byte[] bytes) { try (ObjectInput in = new ObjectInputStream(bis)) { return (Serializable)in.readObject(); } catch (Exception ex) { - throw new RuntimeException("Failed to deserialize state of length " + bytes.length, ex); + throw new DeSerializationFailedException(bytes, ex); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/MdcTriggerInterceptor.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/MdcTriggerInterceptor.java new file mode 100644 index 000000000..2beb9172f --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/interceptor/MdcTriggerInterceptor.java @@ -0,0 +1,34 @@ +package org.sterl.spring.persistent_tasks.trigger.interceptor; + +import org.slf4j.MDC; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; + +/** + * Adds task name and id to the {@link MDC} context. + */ +@Component +public class MdcTriggerInterceptor { + + public static final String TASK_NAME = "taskName"; + public static final String TASK_ID = "taskId"; + + @EventListener + public void beforeRun(TriggerRunningEvent data) { + MDC.put(TASK_NAME, data.key().getTaskName()); + MDC.put(TASK_ID, data.key().getId()); + } + @EventListener + public void onFailed(TriggerFailedEvent data) { + MDC.remove(TASK_NAME); + MDC.remove(TASK_ID); + } + @EventListener + public void onSuccess(TriggerSuccessEvent data) { + MDC.remove(TASK_NAME); + MDC.remove(TASK_ID); + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java new file mode 100644 index 000000000..b1f29fbb8 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java @@ -0,0 +1,41 @@ +package org.sterl.spring.persistent_tasks.trigger.model; + +import java.io.Serializable; +import java.util.Optional; + +import org.springframework.transaction.support.TransactionTemplate; +import org.sterl.spring.persistent_tasks.api.PersistentTask; +import org.sterl.spring.persistent_tasks.shared.model.HasTriggerData; +import org.sterl.spring.persistent_tasks.shared.model.TriggerData; +import org.sterl.spring.persistent_tasks.trigger.component.EditTriggerComponent; + +public record RunTaskWithStateCommand ( + PersistentTask task, + Optional trx, + Serializable state, + TriggerEntity trigger) implements HasTriggerData { + + public Optional execute(EditTriggerComponent editTrigger) { + if (trx.isPresent()) { + return trx.get().execute(t -> runTask(editTrigger)); + } else { + return runTask(editTrigger); + } + } + + private Optional runTask(EditTriggerComponent editTrigger) { + editTrigger.triggerIsNowRunning(trigger, state); + + task.accept(state); + + var result = editTrigger.completeTaskWithSuccess(trigger.getKey(), state); + editTrigger.deleteTrigger(trigger); + + return result; + } + + @Override + public TriggerData getData() { + return trigger.getData(); + } +} 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 f70df765d..b5807f675 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 @@ -16,6 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +import org.springframework.test.context.event.RecordApplicationEvents; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import org.sterl.spring.persistent_tasks.api.PersistentTask; @@ -40,6 +41,7 @@ // @ActiveProfiles("mssql") // postgres mssql mariadb mysql @SpringBootTest(classes = SampleApp.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@RecordApplicationEvents public class AbstractSpringTest { @Autowired 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 f130430ca..f5822bd1e 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 @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.ApplicationEvents; import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.AbstractSpringTest.TaskConfig.Task3; import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; @@ -21,6 +22,11 @@ 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.component.StateSerializer.DeSerializationFailedException; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerAddedEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerCanceledEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent; +import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent; import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity; import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository; @@ -34,6 +40,9 @@ class TriggerServiceTest extends AbstractSpringTest { private TaskRepository taskRepository; @Autowired private TriggerHistoryLastStateRepository triggerHistoryLastStateRepository; + + @Autowired + private ApplicationEvents events; // ensure persistentTask in the spring context @Autowired @@ -66,6 +75,8 @@ void testAddTrigger() throws Exception { // AND assertThat(triggerHistoryLastStateRepository.count()).isZero(); // AND + assertThat(events.stream(TriggerAddedEvent.class).count()).isOne(); + // AND final var e = subject.get(triggerId); assertThat(e).isPresent(); assertThat(e.get().getData().getRunAt().toEpochSecond()).isEqualTo(triggerTime.toEpochSecond()); @@ -87,6 +98,8 @@ void testCreateTrigger() { // THEN assertThat(subject.countTriggers(taskId)).isEqualTo(2); + // AND + assertThat(events.stream(TriggerAddedEvent.class).count()).isEqualTo(2); } @Test @@ -106,6 +119,9 @@ void testCancelTrigger() { assertThat(subject.get(key1)).isEmpty(); assertThat(subject.get(key2)).isPresent(); + + // AND + assertThat(events.stream(TriggerCanceledEvent.class).count()).isOne(); } @Test @@ -119,6 +135,9 @@ void testTriggerSpringSimpleTask() throws Exception { // THEN assertThat(taskRepository.contains(Task3.NAME)).isTrue(); asserts.awaitValue(Task3.NAME + "::trigger3"); + // AND + assertThat(events.stream(TriggerSuccessEvent.class).count()).isOne(); + assertThat(events.stream(TriggerFailedEvent.class).count()).isZero(); } @Test @@ -163,7 +182,7 @@ void testTriggerChainTask() throws Exception { assertThat(e.get().getData().getEnd()).isNotNull(); assertThat(e.get().getData().getExecutionCount()).isOne(); } - + @Test void testFailedIsOnRetry() throws Exception { // GIVEN @@ -179,6 +198,9 @@ void testFailedIsOnRetry() throws Exception { trigger = triggerService.get(trigger.getKey()).get(); assertThat(trigger.getData().getRunAt()).isAfter(OffsetDateTime.now()); assertThat(trigger.getData().getStatus()).isEqualTo(TriggerStatus.WAITING); + // AND + assertThat(events.stream(TriggerSuccessEvent.class).count()).isZero(); + assertThat(events.stream(TriggerFailedEvent.class).count()).isOne(); } @Test @@ -390,6 +412,9 @@ void testBadStateNoRetry() { // WHEN var triggerData = persistentTaskService.getLastTriggerData(t.getKey()).get(); assertThat(triggerData.getStatus()).isEqualTo(TriggerStatus.FAILED); - assertThat(triggerData.getExceptionName()).isEqualTo(RuntimeException.class.getName()); + assertThat(triggerData.getExceptionName()).isEqualTo(DeSerializationFailedException.class.getName()); + // AND + assertThat(events.stream(TriggerSuccessEvent.class).count()).isZero(); + assertThat(events.stream(TriggerFailedEvent.class).count()).isOne(); } } From 67ce1dd1234ed61b08672533c5b5c472982bc225 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:40:31 +0100 Subject: [PATCH 02/10] added release script --- release.sh | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 release.sh diff --git a/release.sh b/release.sh new file mode 100755 index 000000000..ec168ac32 --- /dev/null +++ b/release.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Fetch the current version from the POM +MVN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +echo "Current Maven version: $MVN_VERSION" + +# Release version +RELEASE_VERSION=${MVN_VERSION%-SNAPSHOT} +echo "Releasing version: $RELEASE_VERSION" + +# Set the new release version and tag it in Git +mvn versions:set -DnewVersion="$RELEASE_VERSION" -DgenerateBackupPoms=false +# Deploy the project +mvn clean source:jar javadoc:jar deploy -DskipTests -Prelease +# update git +git add '**/pom.xml' +git commit -am "$RELEASE_VERSION release" +git tag -a "v$RELEASE_VERSION" -m "v$RELEASE_VERSION release" + +# Extract the current version number components +IFS='.' read -r -a VERSION_PARTS <<< "$MVN_VERSION" +MAJOR="${VERSION_PARTS[0]}" +MINOR="${VERSION_PARTS[1]}" +PATCH="${VERSION_PARTS[2]}" +# Increment the patch version for the next snapshot +PATCH=$((PATCH + 1)) +NEXT_VERSION="$MAJOR.$MINOR.$PATCH-SNAPSHOT" +echo "Next development version: $NEXT_VERSION" + +# Set the next snapshot version and commit it in Git +mvn versions:set -DnewVersion="$NEXT_VERSION" -DgenerateBackupPoms=false +git add '**/pom.xml' +git commit -am "Next development version $NEXT_VERSION" + +git push +git push --tags \ No newline at end of file From 9e0ac5a0d6f74852264c519ba76b936e7870e56e Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:41:27 +0100 Subject: [PATCH 03/10] 1.5.4 release --- core/pom.xml | 2 +- db/pom.xml | 2 +- example/pom.xml | 2 +- pom.xml | 2 +- ui/pom.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index b58fe915d..ebc3f3a09 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4-SNAPSHOT + 1.5.4 ../pom.xml diff --git a/db/pom.xml b/db/pom.xml index 08eac60b8..4e48b246d 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4-SNAPSHOT + 1.5.4 ../pom.xml diff --git a/example/pom.xml b/example/pom.xml index 3cf94a778..f08711c96 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4-SNAPSHOT + 1.5.4 ../pom.xml diff --git a/pom.xml b/pom.xml index 18f2bfb0e..a93300210 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4-SNAPSHOT + 1.5.4 pom 2024 diff --git a/ui/pom.xml b/ui/pom.xml index 93b687fed..0932c5dc6 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4-SNAPSHOT + 1.5.4 ../pom.xml From 5787e8f623180f7757c5d570227ba44311571759 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:41:29 +0100 Subject: [PATCH 04/10] Next development version 1.5.5-SNAPSHOT --- core/pom.xml | 2 +- db/pom.xml | 2 +- example/pom.xml | 2 +- pom.xml | 2 +- ui/pom.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index ebc3f3a09..efbfed4fc 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4 + 1.5.5-SNAPSHOT ../pom.xml diff --git a/db/pom.xml b/db/pom.xml index 4e48b246d..b67a2ecdd 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4 + 1.5.5-SNAPSHOT ../pom.xml diff --git a/example/pom.xml b/example/pom.xml index f08711c96..db71642b5 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4 + 1.5.5-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index a93300210..693b98c3d 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4 + 1.5.5-SNAPSHOT pom 2024 diff --git a/ui/pom.xml b/ui/pom.xml index 0932c5dc6..09a564b77 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.4 + 1.5.5-SNAPSHOT ../pom.xml From 8666899481a1c6e06e2c6eddd8cc8590ff7450dd Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:42:27 +0100 Subject: [PATCH 05/10] update to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3135df6eb..cfed45f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v1.5.5 +## v1.5.5 - (2025-01-19) - Added MdcTriggerInterceptor From f2b9ebb4c95317db8192c7bcdf51c0cd2cee4795 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:43:23 +0100 Subject: [PATCH 06/10] include tests in release --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index ec168ac32..5c8addba5 100755 --- a/release.sh +++ b/release.sh @@ -11,7 +11,7 @@ echo "Releasing version: $RELEASE_VERSION" # Set the new release version and tag it in Git mvn versions:set -DnewVersion="$RELEASE_VERSION" -DgenerateBackupPoms=false # Deploy the project -mvn clean source:jar javadoc:jar deploy -DskipTests -Prelease +mvn clean source:jar javadoc:jar deploy -Prelease # update git git add '**/pom.xml' git commit -am "$RELEASE_VERSION release" From 8e138f6f3e2cdb4fcd5b2685fbc32cb3a33b291e Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 13:57:40 +0100 Subject: [PATCH 07/10] adjusted test --- RUN_AND_BUILD.md | 2 +- core/pom.xml | 2 +- .../persistent_tasks/scheduler/SchedulerService.java | 2 ++ .../trigger/component/RunTriggerComponent.java | 10 +++++----- .../trigger/model/RunTaskWithStateCommand.java | 2 ++ .../persistent_tasks/history/HistoryServiceTest.java | 3 ++- db/pom.xml | 2 +- example/pom.xml | 2 +- pom.xml | 2 +- ui/pom.xml | 2 +- 10 files changed, 17 insertions(+), 12 deletions(-) diff --git a/RUN_AND_BUILD.md b/RUN_AND_BUILD.md index b2f5d3ee1..9a6b68018 100644 --- a/RUN_AND_BUILD.md +++ b/RUN_AND_BUILD.md @@ -1,5 +1,5 @@ mvn versions:display-dependency-updates -mvn versions:set -DnewVersion=1.5.3 -DgenerateBackupPoms=false +mvn versions:set -DnewVersion=1.5.5 -DgenerateBackupPoms=false git tag -a v1.5.3 -m "v1.5.3 release" mvn versions:set -DnewVersion=1.5.4-SNAPSHOT -DgenerateBackupPoms=false diff --git a/core/pom.xml b/core/pom.xml index efbfed4fc..b58fe915d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.5-SNAPSHOT + 1.5.4-SNAPSHOT ../pom.xml 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 ecb7aeb63..75787eff6 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 @@ -10,6 +10,7 @@ import java.util.concurrent.Future; import org.springframework.lang.NonNull; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @@ -105,6 +106,7 @@ public List> triggerNextTasks() { * This method should not be called in a transaction! *

*/ + @Transactional(propagation = Propagation.NEVER) @NonNull public List> triggerNextTasks(OffsetDateTime timeDue) { if (taskExecutor.getFreeThreads() > 0) { 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 f09f76335..d0cfeabba 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 @@ -32,19 +32,19 @@ public Optional execute(TriggerEntity trigger) { return Optional.empty(); } - final var taskAndState = getTastAndState(trigger); + final var runTaskWithState = buildTaskWithStateFor(trigger); // something went really wrong this trigger is crap - if (taskAndState == null) return Optional.of(trigger); + if (runTaskWithState == null) return Optional.of(trigger); try { - return taskAndState.execute(editTrigger); + return runTaskWithState.execute(editTrigger); } catch (Exception e) { - return failTaskAndState(taskAndState, e); + return failTaskAndState(runTaskWithState, e); } } @Nullable - private RunTaskWithStateCommand getTastAndState(TriggerEntity trigger) { + private RunTaskWithStateCommand buildTaskWithStateFor(TriggerEntity trigger) { try { final var task = taskService.assertIsKnown(trigger.newTaskId()); final var trx = taskService.getTransactionTemplate(task); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java index b1f29fbb8..cd1cea47a 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java @@ -17,8 +17,10 @@ public record RunTaskWithStateCommand ( public Optional execute(EditTriggerComponent editTrigger) { if (trx.isPresent()) { + System.err.println("IN TRX"); return trx.get().execute(t -> runTask(editTrigger)); } else { + System.err.println("NO TRX"); return runTask(editTrigger); } } 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 c57463b07..66121c174 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 @@ -17,7 +17,8 @@ class HistoryServiceTest extends AbstractSpringTest { - @Autowired HistoryService subject; + @Autowired + private HistoryService subject; @Test void testReQueueTrigger() { diff --git a/db/pom.xml b/db/pom.xml index b67a2ecdd..08eac60b8 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.5-SNAPSHOT + 1.5.4-SNAPSHOT ../pom.xml diff --git a/example/pom.xml b/example/pom.xml index db71642b5..3cf94a778 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.5-SNAPSHOT + 1.5.4-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 693b98c3d..18f2bfb0e 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.5-SNAPSHOT + 1.5.4-SNAPSHOT pom 2024 diff --git a/ui/pom.xml b/ui/pom.xml index 09a564b77..93b687fed 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.5.5-SNAPSHOT + 1.5.4-SNAPSHOT ../pom.xml From 6b91e8526730d6bf884e8f3dc4f2303b6ed3efc5 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 14:02:48 +0100 Subject: [PATCH 08/10] fixed transaction management --- .../trigger/component/EditTriggerComponent.java | 2 ++ .../persistent_tasks/trigger/model/RunTaskWithStateCommand.java | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) 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 b0363f403..13ef05534 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 @@ -10,6 +10,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.sterl.spring.persistent_tasks.api.AddTriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerKey; @@ -148,6 +149,7 @@ public int markTriggersAsRunning(Collection keys, String runOn) { OffsetDateTime.now(), TriggerStatus.RUNNING); } + @Transactional(propagation = Propagation.SUPPORTS) public void triggerIsNowRunning(TriggerEntity trigger, Serializable state) { if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn()); publisher.publishEvent(new TriggerRunningEvent( diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java index cd1cea47a..b1f29fbb8 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/model/RunTaskWithStateCommand.java @@ -17,10 +17,8 @@ public record RunTaskWithStateCommand ( public Optional execute(EditTriggerComponent editTrigger) { if (trx.isPresent()) { - System.err.println("IN TRX"); return trx.get().execute(t -> runTask(editTrigger)); } else { - System.err.println("NO TRX"); return runTask(editTrigger); } } From 0fd28c6d028301ea2cd1695a019399348dd42224 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 14:17:45 +0100 Subject: [PATCH 09/10] cleanup the doc --- README.md | 77 ++++++++----------------------------------------------- 1 file changed, 10 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 06abcd5ce..fcfe0ff1c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ Focus is the usage with spring boot and JPA. Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw) +# Documentation + +Use for more advanced doc the [WIKI](https://github.com/sterlp/spring-persistent-tasks/wiki). +The README contains a shorter how to use. + # DBs for storage ## Tested in the pipeline @@ -27,11 +32,11 @@ Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw) - mySQL: sequences are not supported -# Setup and Run a Task +# JavaDoc - [JavaDoc](https://sterlp.github.io/spring-persistent-tasks/javadoc-core/org/sterl/spring/persistent_tasks/PersistentTaskService.html) -## Maven +# Maven setup - [Maven Central spring-persistent-tasks-core](https://central.sonatype.com/artifact/org.sterl.spring/spring-persistent-tasks-core/versions) @@ -43,7 +48,7 @@ Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw) ``` -## Setup Spring +# Setup Spring ```java @SpringBootApplication @@ -131,62 +136,19 @@ public void buildVehicle() { final var v = new Vehicle(); // set any data to v ... - // EITHER: queue it, will run later + // EITHER: queue it - will always run later triggerService.queue(BuildVehicleTask.ID.newUniqueTrigger(v)); - // OR: will queue it and run it if possible. + // OR: will queue it and run it now if possible. // if the scheduler service is missing it is same as using the TriggerService persistentTaskService.runOrQueue(BuildVehicleTask.ID.newUniqueTrigger(v)); } ``` -### Build complex Trigger - -```java -private final PersistentTaskService persistentTaskService; - -public void buildVehicle() { - var trigger = TaskTriggerBuilder - .newTrigger("task2") - .id("my-id") // will overwrite existing triggers - .state(new Vehicle("funny")) - .runAfter(Duration.ofHours(2)) - .build(); - - persistentTaskService.runOrQueue(trigger); -} -``` - -### Use a Spring Event - -```java -private final ApplicationEventPublisher eventPublisher; - -public void buildVehicle() { - // Vehicle has to be Serializable - final var v = new Vehicle(); - // send an event with the trigger inside - same as calling the PersistentTaskService - eventPublisher.publishEvent(TriggerTaskCommand.of(BuildVehicleTask.ID.newUniqueTrigger(v))); -} -``` - ### JUnit Tests - [Persistent Task and Testing](https://github.com/sterlp/spring-persistent-tasks/wiki/Triggers-and-Tasks-in-JUnit-Tests) - -### Spring configuration options - -| Property | Type | Description | Default Value | -| ---------------------------------------------- | -------------------- | ------------------------------------------------------------------------ | ------------------ | -| `spring.persistent-tasks.poll-rate` | `java.lang.Integer` | The interval at which the scheduler checks for new tasks, in seconds. | `30` | -| `spring.persistent-tasks.max-threads` | `java.lang.Integer` | The number of threads to use; set to 0 to disable task processing. | `10` | -| `spring.persistent-tasks.task-timeout` | `java.time.Duration` | The maximum time allowed for a task and scheduler to complete a task. | `PT5M` (5 minutes) | -| `spring.persistent-tasks.poll-task-timeout` | `java.lang.Integer` | The interval at which the system checks for abandoned tasks, in seconds. | `300` (5 minutes) | -| `spring.persistent-tasks.scheduler-enabled` | `java.lang.Boolean` | Indicates whether this node should handle triggers. | `true` | -| `spring.persistent-tasks.history.delete-after` | `java.time.Duration` | The max age for a trigger in the hstory. | `PT72H` (30 days) | -| `spring.persistent-tasks.history.delete-rate` | `java.time.Integer` | The interval at which old triggers are deleted, in hours. | `24` (24 hours) | - # Setup DB with Liquibase Liquibase is supported. Either import all or just the required versions. @@ -246,25 +208,6 @@ public class ExampleApplication { ![History](screenshots/history-screen.png) -## Spring Boot CSRF config for the UI - -Axios should work with the following spring config out of the box with csrf: - -```java -@Bean -SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .httpBasic(org.springframework.security.config.Customizer.withDefaults()) - .csrf(c -> - c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) - ); - return http.build(); -} -``` - -more informations: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html - # Alternatives - quartz From 526449164b18492067ac49f1563721e452233ff6 Mon Sep 17 00:00:00 2001 From: Paul Sterl Date: Sun, 19 Jan 2025 14:25:39 +0100 Subject: [PATCH 10/10] cleanup the doc --- README.md | 89 ++++++++----------------------------------------------- 1 file changed, 12 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index fcfe0ff1c..105ab26ee 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,13 @@ The README contains a shorter how to use. - [JavaDoc](https://sterlp.github.io/spring-persistent-tasks/javadoc-core/org/sterl/spring/persistent_tasks/PersistentTaskService.html) -# Maven setup +# Quickstart - [Maven Central spring-persistent-tasks-core](https://central.sonatype.com/artifact/org.sterl.spring/spring-persistent-tasks-core/versions) +## Setup with Maven + + ```xml org.sterl.spring @@ -48,7 +51,7 @@ The README contains a shorter how to use. ``` -# Setup Spring +## Setup Spring ```java @SpringBootApplication @@ -56,61 +59,7 @@ The README contains a shorter how to use. public class ExampleApplication { ``` -## Setup a spring persistent task - -### As a class - -```java -@Component(BuildVehicleTask.NAME) -@RequiredArgsConstructor -@Slf4j -public class BuildVehicleTask implements PersistentTask { - - private static final String NAME = "buildVehicleTask"; - public static final TaskId ID = new TaskId<>(NAME); - - private final VehicleRepository vehicleRepository; - - @Override - public void accept(Vehicle vehicle) { - // do stuff - // save - vehicleRepository.save(vehicle); - } - // OPTIONAL - @Override - public RetryStrategy retryStrategy() { - // run 5 times, multiply the execution count with 4, add the result in HOURS to now. - return new MultiplicativeRetryStrategy(5, ChronoUnit.HOURS, 4) - } - // OPTIONAL - // if the task in accept requires a DB transaction, join them together with the framework - // if true the TransactionTemplate is used. Set here any timeouts. - @Override - public boolean isTransactional() { - return true; - } -} -``` - -Consider setting a timeout to the `TransactionTemplate`: - -```java -@Bean -TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { - TransactionTemplate template = new TransactionTemplate(transactionManager); - template.setTimeout(10); - return template; -} -``` - -### As a closure - -Simple task will use defaults: - -- Not a transactional task, e.g. HTTP calls -- 4 executions, one regular and 3 retries, linear -- using minutes with an offset of 1 which is added to now +## Create a Task ```java @Bean @@ -119,29 +68,15 @@ PersistentTask task1(VehicleHttpConnector vehicleHttpConnector) { } ``` -### Task Transaction Management - -[Transaction-Management Task](https://github.com/sterlp/spring-persistent-tasks/wiki/Transaction-Management) - -## Queue a task execution - -### Direct usage of the `TriggerService` or `PersistentTaskService`. +## Trigger a task ```java -private final TriggerService triggerService; -private final PersistentTaskService persistentTaskService; - -public void buildVehicle() { - // Vehicle has to be Serializable - final var v = new Vehicle(); - // set any data to v ... - - // EITHER: queue it - will always run later - triggerService.queue(BuildVehicleTask.ID.newUniqueTrigger(v)); +@Autowired +PersistentTaskService persistentTaskService; - // OR: will queue it and run it now if possible. - // if the scheduler service is missing it is same as using the TriggerService - persistentTaskService.runOrQueue(BuildVehicleTask.ID.newUniqueTrigger(v)); +public void triggerTask1(Vehicle vehicle) { + persistentTaskService.runOrQueue( + TaskTriggerBuilder.newTrigger("task1").state(vehicle).build()); } ```