diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ffdde1ce..cfed45f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.5.5 - (2025-01-19) + +- Added MdcTriggerInterceptor + ## v1.5.4 - (2025-01-14) - adjusted trigger cols that the UI does not break diff --git a/README.md b/README.md index 06abcd5ce..105ab26ee 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,14 +32,17 @@ 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 +# 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 @@ -51,61 +59,7 @@ Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw) 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 @@ -114,59 +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`. - -```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 run later - triggerService.queue(BuildVehicleTask.ID.newUniqueTrigger(v)); - - // OR: will queue it and run it 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 +## Trigger a task ```java -private final ApplicationEventPublisher eventPublisher; +@Autowired +PersistentTaskService persistentTaskService; -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))); +public void triggerTask1(Vehicle vehicle) { + persistentTaskService.runOrQueue( + TaskTriggerBuilder.newTrigger("task1").state(vehicle).build()); } ``` @@ -174,19 +84,6 @@ public void buildVehicle() { - [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 +143,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 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/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/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/EditTriggerComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/EditTriggerComponent.java index f03c9cbc5..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; @@ -18,6 +19,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 +42,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 +148,11 @@ public int markTriggersAsRunning(Collection keys, String runOn) { return triggerRepository.markTriggersAsRunning(keys, 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( + 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..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 @@ -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); + + 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.call(); + return runTaskWithState.execute(editTrigger); } catch (Exception e) { - return failTaskAndState(taskAndState, e); + return failTaskAndState(runTaskWithState, e); } } @Nullable - private TaskAndState getTastAndState(TriggerEntity trigger) { + private RunTaskWithStateCommand buildTaskWithStateFor(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/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/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(); } } diff --git a/release.sh b/release.sh new file mode 100755 index 000000000..5c8addba5 --- /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 -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