diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1398d8d..65159df61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.4.0 - (2025-01-02) + +- @Transactional Annotation support +- PersistentTask instead of Task or SpringBeanTask + + +## v1.3.1 - (2025-01-02) + +- Bugfixes +- Sprign Transaction Template support + ## v1.3.0 - (2025-01-01) - MariaDB support diff --git a/README.md b/README.md index 61fd3012b..7817dac9a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ public class ExampleApplication { @Component(BuildVehicleTask.NAME) @RequiredArgsConstructor @Slf4j -public class BuildVehicleTask implements SpringBeanTask { +public class BuildVehicleTask implements PersistentTask { private static final String NAME = "buildVehicleTask"; public static final TaskId ID = new TaskId<>(NAME); @@ -109,11 +109,15 @@ Simple task will use defaults: ```java @Bean -SpringBeanTask task1(VehicleHttpConnector vehicleHttpConnector) { +PersistentTask task1(VehicleHttpConnector vehicleHttpConnector) { return v -> vehicleHttpConnector.send(v); } ``` +### 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`. diff --git a/RUN_AND_BUILD.md b/RUN_AND_BUILD.md index 0e998e949..9ac78e6c3 100644 --- a/RUN_AND_BUILD.md +++ b/RUN_AND_BUILD.md @@ -1,6 +1,6 @@ mvn versions:display-dependency-updates -mvn versions:set -DnewVersion=1.3.1 -DgenerateBackupPoms=false -mvn versions:set -DnewVersion=1.3.2-SNAPSHOT -DgenerateBackupPoms=false +mvn versions:set -DnewVersion=1.4.0-SNAPSHOT -DgenerateBackupPoms=false +mvn versions:set -DnewVersion=1.4.0 -DgenerateBackupPoms=false ## postgres docker run --name pg-container -e POSTGRES_USER=sa -e POSTGRES_PASSWORD=veryStrong123 -p 5432:5432 -d postgres diff --git a/core/pom.xml b/core/pom.xml index d45f6ead2..bc3ab7a68 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.3.2-SNAPSHOT + 1.4.0-SNAPSHOT ../pom.xml diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java index e51e0d8b8..143b8c8a2 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/AddTriggerRequest.java @@ -5,7 +5,7 @@ /** /** - * For any registered task a task trigger represent one unit of work, executing this task once. + * For any registered persistentTask a persistentTask trigger represent one unit of work, executing this persistentTask once. * @param state type which has to be of {@link Serializable} */ public record AddTriggerRequest( diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/PersistentTask.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/PersistentTask.java new file mode 100644 index 000000000..a14d2bdbf --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/PersistentTask.java @@ -0,0 +1,34 @@ +package org.sterl.spring.persistent_tasks.api; + +import java.io.Serializable; + +/** + * A Spring persistent task whose state is saved in a {@link Trigger}. + * + *

This interface defines a task that accepts a state of type T and + * provides default implementations for retry strategies. + * + * @param the type of the state, which must be {@link Serializable} + */ +@FunctionalInterface +public interface PersistentTask { + void accept(T state); + + default RetryStrategy retryStrategy() { + return RetryStrategy.THREE_RETRIES; + } + + /** + * Whether the persistentTask is transaction or not. If true the execution + * is wrapped into the default transaction template together with the state update + * and the following events: + *

    + *
  1. org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent
  2. + *
  3. org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent
  4. + *
+ * @return {@code true} if the persistentTask is transactional; {@code false} otherwise. + */ + default boolean isTransactional() { + return false; + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/SpringBeanTask.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/SpringBeanTask.java index 83f302e4a..3953812fe 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/SpringBeanTask.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/SpringBeanTask.java @@ -1,29 +1,12 @@ package org.sterl.spring.persistent_tasks.api; import java.io.Serializable; -import java.util.function.Consumer; +/** + * Same as {@link PersistentTask} + */ +@Deprecated @FunctionalInterface -public interface SpringBeanTask extends Consumer { - @Override - void accept(T state); - - default RetryStrategy retryStrategy() { - return RetryStrategy.THREE_RETRIES; - } +public interface SpringBeanTask extends PersistentTask { - /** - * Whether the task is transaction or not. If true the execution - * is wrapped into the default transaction template together with the state update - * and the following events: - *
    - *
  1. org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent
  2. - *
  3. org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent
  4. - *
  5. org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent
  6. - *
- * @return {@code true} if the task is transactional; {@code false} otherwise. - */ - default boolean isTransactional() { - return false; - } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/Task.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/Task.java deleted file mode 100644 index 7cccd16a9..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/Task.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.sterl.spring.persistent_tasks.api; - -import java.io.Serializable; - -import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder; - -public interface Task extends SpringBeanTask { - TaskId getId(); - - default TaskTriggerBuilder newTrigger() { - return getId().newTrigger(); - } -} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java index 0daaf9601..1c631af8f 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java @@ -8,7 +8,7 @@ import lombok.RequiredArgsConstructor; /** - * Represents the ID of a task, which is currently not running. + * Represents the ID of a persistentTask, which is currently not running. */ public record TaskId(String name) implements Serializable { diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/TransactionalTask.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TransactionalTask.java new file mode 100644 index 000000000..f8abd2948 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/TransactionalTask.java @@ -0,0 +1,30 @@ +package org.sterl.spring.persistent_tasks.api; + +import java.io.Serializable; + +/** + * Similar to {@link PersistentTask} but specifically for transactional workloads. + * Use this interface when the task execution should be wrapped in a transaction. + * + *

This interface ensures that the task's execution is transactional, meaning that it will + * be executed within a transaction context, along with the state update and the dispatching of + * relevant events. + * + * @param the type of the state, which must be {@link Serializable} + */ +@FunctionalInterface +public interface TransactionalTask extends PersistentTask { + /** + * Whether the persistentTask is transaction or not. If true the execution + * is wrapped into the default transaction template together with the state update + * and the following events: + *

    + *
  1. org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent
  2. + *
  3. org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent
  4. + *
+ * @return {@code true} if the persistentTask is transactional; {@code false} otherwise. + */ + default boolean isTransactional() { + return true; + } +} 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 ce8c5cec8..881448cb1 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 @@ -32,7 +32,7 @@ public TaskId toTaskId() { return new TaskId<>(taskName); } /** - * Builds a trigger for the given task name + * Builds a trigger for the given persistentTask name */ public TriggerKey(String taskName) { id = UUID.randomUUID().toString(); @@ -40,7 +40,7 @@ public TriggerKey(String taskName) { } /** - * Just triggers the given task to be executed using null as state. + * Just triggers the given persistentTask to be executed using null as state. */ public AddTriggerRequest newTrigger(TaskId taskId) { return newTrigger(taskId, null); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java index 5ce158a09..05485691e 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java @@ -9,7 +9,7 @@ import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder; /** - * An event to trigger one or multiple task executions + * An event to trigger one or multiple persistentTask executions */ public record TriggerTaskCommand(Collection> triggers) { 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 195153cd1..93070040c 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 @@ -90,7 +90,7 @@ public Optional findStatus(String name) { } /** - * Simply triggers the next task which is now due to be executed + * Simply triggers the next persistentTask which is now due to be executed */ @NonNull public List> triggerNextTasks() { @@ -134,7 +134,7 @@ public Future runOrQueue( trigger = triggerService.markTriggersAsRunning(trigger, name); pingRegistry().addRunning(1); } else { - log.debug("Currently not enough free thread available {} of {} in use. Task {} queued.", + log.debug("Currently not enough free thread available {} of {} in use. PersistentTask {} queued.", taskExecutor.getFreeThreads(), taskExecutor.getMaxThreads(), trigger.getKey()); } return trigger; diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java index e2717cfd5..51d6d77f2 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/scheduler/SchedulerTimer.java @@ -35,7 +35,7 @@ void triggerNextTasks() { } } - @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-task-timeout:300}", timeUnit = TimeUnit.SECONDS) + @Scheduled(fixedDelayString = "${spring.persistent-tasks.poll-persistentTask-timeout:300}", timeUnit = TimeUnit.SECONDS) void rescheduleAbandonedTasks() { var timeout = OffsetDateTime.now().minus(taskTimeout); for (SchedulerService s : schedulerServices) { 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 c96673af4..9591d6cca 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 @@ -9,10 +9,10 @@ import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; -import org.sterl.spring.persistent_tasks.api.Task; +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.task.model.RegisteredTask; +import org.sterl.spring.persistent_tasks.task.component.TaskTransactionComponent; import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; import lombok.RequiredArgsConstructor; @@ -22,6 +22,7 @@ @RequiredArgsConstructor public class TaskService { + private final TaskTransactionComponent taskTransactionComponent; private final TaskRepository taskRepository; @Transactional(readOnly = true) @@ -29,62 +30,67 @@ public Set> findAllTaskIds() { return this.taskRepository.all(); } - - public Optional> get(TaskId id) { + public Optional> get(TaskId id) { return taskRepository.get(id); } + + public Optional getTransactionTemplate( + PersistentTask task) { + return taskTransactionComponent.getTransactionTemplate(task); + } /** - * Check if the {@link Task} is known or not. + * Check if the {@link PersistentTask} is known or not. * * @param the state type - * @param id the {@link TaskId} of the {@link Task} + * @param id the {@link TaskId} of the {@link PersistentTask} * @throws IllegalStateException if the id is unknown - * @return the {@link Task} registered to the given id + * @return the {@link PersistentTask} registered to the given id */ @NonNull - public Task assertIsKnown(@NonNull TaskId id) { + public PersistentTask assertIsKnown(@NonNull TaskId id) { final var task = taskRepository.get(id); if (task.isEmpty()) { - throw new IllegalStateException("Task with ID " + id + throw new IllegalStateException("PersistentTask with ID " + id + " is unknown. Known tasks: " + taskRepository.all()); } return task.get(); } /** - * A way to manually register a task, usually better to use {@link SpringBeanTask}. - */ - public TaskId register(String name, Consumer task) { - RegisteredTask t = new RegisteredTask<>(name, task); - return register(t); - } - /** - * A way to manually register a task, usually not needed as spring beans will be added automatically. + * A way to manually register a persistentTask, usually better to use {@link PersistentTask}. */ - public TaskId register(String name, SpringBeanTask task) { - RegisteredTask t = new RegisteredTask<>(name, task); - return register(t); + public TaskId register(String name, Consumer task) { + return register(name, new PersistentTask() { + @Override + public void accept(Serializable state) { + task.accept(state); + } + }); } /** - * A way to manually register a task, usually not needed as spring beans will be added anyway. + * A way to manually register a persistentTask, usually not needed as spring beans will be added automatically. */ - public TaskId register(RegisteredTask task) { - return taskRepository.addTask(task); + @SuppressWarnings("unchecked") + public TaskId register(String name, PersistentTask task) { + var id = (TaskId)TaskId.of(name); + return register(id, task); } - /** - * A way to manually register a task, usually not needed as spring beans will be added anyway. + * A way to manually register a persistentTask, usually not needed as spring beans will be added automatically. */ - public TaskId replace(RegisteredTask task) { - taskRepository.remove(task); - return register(task); + public TaskId register(TaskId id, PersistentTask task) { + // init any transaction as needed + taskTransactionComponent.getTransactionTemplate(task); + return taskRepository.addTask(id, task); } /** - * A way to manually register a task, usually not needed as spring beans will be added automatically. + * A way to manually register a persistentTask, usually not needed as spring beans will be added automatically. */ - public TaskId replace(String name, SpringBeanTask task) { - RegisteredTask t = new RegisteredTask<>(name, task); - return replace(t); + @SuppressWarnings("unchecked") + public TaskId replace(String name, PersistentTask task) { + var id = (TaskId)TaskId.of(name); + taskRepository.remove(id); + return register(id, task); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java new file mode 100644 index 000000000..bbe03f180 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/component/TaskTransactionComponent.java @@ -0,0 +1,81 @@ +package org.sterl.spring.persistent_tasks.task.component; + +import java.io.Serializable; +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; +import org.sterl.spring.persistent_tasks.api.PersistentTask; +import org.sterl.spring.persistent_tasks.task.util.ReflectionUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TaskTransactionComponent { + + private final PlatformTransactionManager transactionManager; + private final TransactionTemplate template; + private final Set joinTransaction = EnumSet.of( + Propagation.MANDATORY, Propagation.REQUIRED, Propagation.SUPPORTS); + private final Map, Optional> cache = new ConcurrentHashMap<>(); + + public Optional getTransactionTemplate(PersistentTask task) { + if (cache.containsKey(task)) return cache.get(task); + + Optional result; + // first we apply a default + if (task.isTransactional()) result = Optional.of(template); + else result = Optional.empty(); + + var annotation = ReflectionUtil.getAnnotation(task, Transactional.class); + if (annotation != null) { + log.debug("found {} on task={}, creating custom ", annotation, task.getClass().getName()); + result = Optional.ofNullable(builTransactionTemplate(task, annotation)); + } + cache.put(task, result); + return result; + } + + private TransactionTemplate builTransactionTemplate(PersistentTask task, Transactional annotation) { + TransactionTemplate result; + if (joinTransaction.contains(annotation.propagation())) { + // No direct mapping for 'rollbackFor' or 'noRollbackFor' + if (annotation.noRollbackFor().length > 0 || annotation.rollbackFor().length > 0) { + throw new IllegalArgumentException("noRollbackFor or rollbackFor not supported. Please remove the settings on " + + task.getClass()); + } else { + var dev = convertTransactionalToDefinition(annotation); + dev.setName(task.getClass().getSimpleName()); + result = new TransactionTemplate(transactionManager, dev); + } + } else { + log.info("Propagation={} disables join of transaction for {}", + annotation.propagation(), task.getClass().getName()); + result = null; + } + return result; + } + + static DefaultTransactionDefinition convertTransactionalToDefinition(Transactional transactional) { + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + + // Map Transactional attributes to DefaultTransactionDefinition + def.setIsolationLevel(transactional.isolation().value()); + def.setPropagationBehavior(Propagation.REQUIRED.value()); + def.setTimeout(transactional.timeout()); + def.setReadOnly(false); + + return def; + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/config/TaskConfig.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/config/TaskConfig.java index c78389342..f27d89762 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/task/config/TaskConfig.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/config/TaskConfig.java @@ -1,15 +1,14 @@ package org.sterl.spring.persistent_tasks.task.config; +import java.io.Serializable; import java.util.Map.Entry; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; +import org.sterl.spring.persistent_tasks.api.PersistentTask; import org.sterl.spring.persistent_tasks.api.TaskId; -import org.sterl.spring.persistent_tasks.task.model.RegisteredTask; -import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; +import org.sterl.spring.persistent_tasks.task.TaskService; import lombok.extern.slf4j.Slf4j; @@ -19,23 +18,20 @@ public class TaskConfig { @SuppressWarnings({ "rawtypes", "unchecked" }) @Autowired void configureSimpleTasks(GenericApplicationContext context, - TaskRepository taskRepository) { - final var simpleTasks = context.getBeansOfType(SpringBeanTask.class); - for(Entry t : simpleTasks.entrySet()) { - final var registeredTask = new RegisteredTask<>(t.getKey(), t.getValue()); - taskRepository.addTask(registeredTask); + TaskService taskService) { + final var simpleTasks = context.getBeansOfType(PersistentTask.class); + for(Entry t : simpleTasks.entrySet()) { + var id = taskService.register(t.getKey(), t.getValue()); - addTaskIdIfMissing(context, registeredTask); + addTaskIdIfMissing(context, id); } } - private void addTaskIdIfMissing(GenericApplicationContext context, final RegisteredTask registeredTask) { - final var taskIdContextName = registeredTask.getId().name() + "Id"; + private void addTaskIdIfMissing(GenericApplicationContext context, + TaskId id) { + final var taskIdContextName = id.name() + "Id"; if (!context.containsBean(taskIdContextName)) { - log.info("Adding TaskId={} with name={} to spring context", registeredTask.getId(), taskIdContextName); - var beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(registeredTask.getId().getClass()); - context.registerBean(taskIdContextName, - TaskId.class, () -> registeredTask.getId()); + log.info("Adding {} with name={} to spring context", id, taskIdContextName); + context.registerBean(taskIdContextName, TaskId.class, () -> id); } } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/model/RegisteredTask.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/model/RegisteredTask.java deleted file mode 100644 index e7245a629..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/task/model/RegisteredTask.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.sterl.spring.persistent_tasks.task.model; - -import java.io.Serializable; -import java.util.function.Consumer; - -import org.sterl.spring.persistent_tasks.api.RetryStrategy; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; -import org.sterl.spring.persistent_tasks.api.Task; -import org.sterl.spring.persistent_tasks.api.TaskId; - -import lombok.Getter; - -public class RegisteredTask implements Task { - - @Getter - private final TaskId id; - private final SpringBeanTask fun; - @Getter - private RetryStrategy retryStrategy = RetryStrategy.THREE_RETRIES; - - public RegisteredTask(String name, SpringBeanTask fun) { - super(); - this.id = new TaskId<>(name); - this.fun = fun; - } - - public RegisteredTask(String name, Consumer consumer) { - this(name, s -> { - consumer.accept(s); - }); - } - - @Override - public void accept(T state) { - this.fun.accept(state); - } - @Override - public RetryStrategy retryStrategy() { - return this.fun.retryStrategy(); - } - @Override - public boolean isTransactional() { - return this.fun.isTransactional(); - } -} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/repository/TaskRepository.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/repository/TaskRepository.java index 5e3d46e6b..fc8d3ee57 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/task/repository/TaskRepository.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/repository/TaskRepository.java @@ -2,7 +2,6 @@ import java.io.Serializable; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -10,7 +9,7 @@ import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; -import org.sterl.spring.persistent_tasks.api.Task; +import org.sterl.spring.persistent_tasks.api.PersistentTask; import org.sterl.spring.persistent_tasks.api.TaskId; import lombok.extern.slf4j.Slf4j; @@ -18,56 +17,48 @@ @Slf4j @Component public class TaskRepository { - private final Map, Task> tasks = new ConcurrentHashMap<>(); + private final Map, PersistentTask> persistentTasks = new ConcurrentHashMap<>(); - public TaskRepository(List> tasks) { - super(); - for (Task task : tasks) { - addTask(task); - } - } - - @SuppressWarnings("unchecked") - public Task remove(Task task) { - if (task == null) { + public PersistentTask remove(TaskId taskId) { + if (taskId == null) { return null; } - return (Task)tasks.remove(task.getId()); + return persistentTasks.remove(taskId); } - public TaskId addTask(Task task) { - if (contains(task.getId())) { - throw new IllegalStateException("The task id " + task.getId() + " is already used!"); + public TaskId addTask(@NonNull TaskId taskId, PersistentTask task) { + if (contains(taskId)) { + throw new IllegalStateException("The " + taskId + " is already used!"); } - log.info("Adding task={} to={}", task.getId(), task.getClass()); - this.tasks.put(task.getId(), task); - return task.getId(); + log.info("Adding {} to={}", taskId, task.getClass()); + this.persistentTasks.put(taskId, task); + return taskId; } @NonNull @SuppressWarnings("unchecked") - public Optional> get(@NonNull TaskId taskId) { + public Optional> get(@NonNull TaskId taskId) { assert taskId != null; - return Optional.ofNullable((Task)tasks.get(taskId)); + return Optional.ofNullable((PersistentTask)persistentTasks.get(taskId)); } /** - * Removes all tasks, should only be used for testing! + * Removes all persistentTasks, should only be used for testing! */ public void clear() { - log.warn("*** All tasks {} will be removed now! ***", tasks.size()); - tasks.clear(); + log.warn("*** All persistentTasks {} will be removed now! ***", persistentTasks.size()); + persistentTasks.clear(); } public boolean contains(String name) { return contains(new TaskId<>(name)); } - public boolean contains(TaskId id) { - return tasks.containsKey(id); + public boolean contains(TaskId id) { + return persistentTasks.containsKey(id); } public Set> all() { - return new HashSet<>(tasks.keySet()); + return new HashSet<>(persistentTasks.keySet()); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/task/util/ReflectionUtil.java b/core/src/main/java/org/sterl/spring/persistent_tasks/task/util/ReflectionUtil.java new file mode 100644 index 000000000..47726215d --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/task/util/ReflectionUtil.java @@ -0,0 +1,24 @@ +package org.sterl.spring.persistent_tasks.task.util; + +import java.io.Serializable; +import java.lang.annotation.Annotation; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; +import org.sterl.spring.persistent_tasks.api.PersistentTask; + +public abstract class ReflectionUtil { + + public static A getAnnotation(PersistentTask inTask, Class searchFor) { + var task = AopProxyUtils.ultimateTargetClass(inTask); + A result = AnnotationUtils.findAnnotation(task, searchFor); + if (result != null) return result; + + var targetMethod = ReflectionUtils.findMethod(task, "accept", Serializable.class); + if (targetMethod == null) return null; + + result = AnnotationUtils.findAnnotation(targetMethod, searchFor); + return result; + } +} 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 782b2a372..9e1f66fa9 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 @@ -124,7 +124,7 @@ public TriggerEntity queue(AddTriggerRequest tigger) } /** - * If you changed your mind, cancel the task + * If you changed your mind, cancel the persistentTask */ public Optional cancel(TriggerKey key) { return editTrigger.cancelTask(key); 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 de15e5308..afac1a966 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 @@ -3,13 +3,12 @@ import java.io.Serializable; import java.time.OffsetDateTime; import java.util.Optional; -import java.util.concurrent.Callable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionTemplate; -import org.sterl.spring.persistent_tasks.api.Task; +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.TriggerEntity; @@ -25,7 +24,6 @@ public class RunTriggerComponent { private final TaskService taskService; private final EditTriggerComponent editTrigger; private final ApplicationEventPublisher eventPublisher; - private final TransactionTemplate trx; private final StateSerializer serializer = new StateSerializer(); /** @@ -40,49 +38,49 @@ public Optional execute(TriggerEntity trigger) { if (taskAndState == null) return Optional.of(trigger); try { - Optional result; - if (taskAndState.isTransactional()) { - result = trx.execute(t -> taskAndState.call()); - } else { - result = taskAndState.call(); - } - - return result; + return taskAndState.call(); } catch (Exception e) { return handleTaskException(taskAndState, e); } } + @Nullable private TaskAndState 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, state, trigger); + return new TaskAndState(task, trx, state, trigger); } catch (Exception e) { // this trigger is somehow crap, no retry and done. - handleTaskException(new TaskAndState(null, null, trigger), e); + handleTaskException(new TaskAndState(null, Optional.empty(), null, trigger), e); return null; } } @RequiredArgsConstructor - private class TaskAndState implements Callable> { - final Task task; + private class TaskAndState { + final PersistentTask persistentTask; + final Optional trx; final Serializable state; final TriggerEntity trigger; - boolean isTransactional() { - return task.isTransactional(); + Optional call() { + if (trx.isPresent()) { + return trx.get().execute(t -> runTask()); + } else { + return runTask(); + } } - public Optional call() { + + private Optional runTask() { eventPublisher.publishEvent(new TriggerRunningEvent(trigger)); - task.accept(state); + persistentTask.accept(state); var result = editTrigger.completeTaskWithSuccess(trigger.getKey()); editTrigger.deleteTrigger(trigger); return result; - } } @@ -90,20 +88,21 @@ private Optional handleTaskException(TaskAndState taskAndState, @Nullable Exception e) { var trigger = taskAndState.trigger; - var task = taskAndState.task; + var task = taskAndState.persistentTask; var result = editTrigger.completeTaskWithStatus(trigger.getKey(), e); - if (task != null && - task.retryStrategy().shouldRetry(trigger.getData().getExecutionCount(), 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(), - result.get().getData().getRunAt(), - result.get().getData().getStatus(), + data.getRunAt(), + data.getStatus(), e); } else { log.error("Trigger with key={} not found and may be at a wrong state!", @@ -117,5 +116,4 @@ private Optional handleTaskException(TaskAndState taskAndState, } return result; } - } 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 a9879cce6..fe4a4e26c 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 @@ -69,7 +69,7 @@ public TriggerKey getKey() { public TriggerEntity cancel() { this.data.setEnd(OffsetDateTime.now()); this.data.setStatus(TriggerStatus.CANCELED); - this.data.setExceptionName("Task canceled"); + this.data.setExceptionName("PersistentTask canceled"); this.data.setRunningDurationInMs(null); return this; } 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 1606ed4ea..6e943192b 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 @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; +import org.sterl.spring.persistent_tasks.api.PersistentTask; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.event.TriggerTaskCommand; import org.sterl.spring.persistent_tasks.history.HistoryService; @@ -111,10 +111,10 @@ SchedulerService schedulerB(TriggerService triggerService, EditSchedulerStatusCo } /** - * This task will trigger task2 + * This persistentTask will trigger task2 */ @Bean - SpringBeanTask task1(ApplicationEventPublisher publisher, AsyncAsserts asserts) { + PersistentTask task1(ApplicationEventPublisher publisher, AsyncAsserts asserts) { return (String state) -> { asserts.info("task1::" + state); publisher.publishEvent(TriggerTaskCommand.of("task2", "task1::" + state)); @@ -122,13 +122,13 @@ SpringBeanTask task1(ApplicationEventPublisher publisher, AsyncAsserts a } @Bean - SpringBeanTask task2(AsyncAsserts asserts) { + PersistentTask task2(AsyncAsserts asserts) { return state -> asserts.info("task2::" + state); } @Component(Task3.NAME) @RequiredArgsConstructor - public static class Task3 implements SpringBeanTask { + public static class Task3 implements PersistentTask { public static final String NAME = "task3"; public static final TaskId ID = new TaskId<>(NAME); @@ -141,7 +141,7 @@ public void accept(String state) { } @Bean - SpringBeanTask slowTask(AsyncAsserts asserts) { + PersistentTask slowTask(AsyncAsserts asserts) { return sleepTime -> { try { if (sleepTime == null) { @@ -163,7 +163,6 @@ protected Optional runNextTrigger() { @BeforeEach public void beforeEach() throws Exception { - hibernateAsserts.reset(); triggerService.deleteAll(); historyService.deleteAll(); asserts.clear(); @@ -171,6 +170,7 @@ public void beforeEach() throws Exception { schedulerB.setMaxThreads(20); schedulerA.start(); schedulerB.start(); + hibernateAsserts.reset(); } @AfterEach 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 fb4fd2332..8a42eb481 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 @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; import org.sterl.spring.persistent_tasks.api.RetryStrategy; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; +import org.sterl.spring.persistent_tasks.api.PersistentTask; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus; @@ -17,7 +17,7 @@ class TaskSchedulerServiceTest extends AbstractSpringTest { void testFailedTasksAreRetried() throws Exception { // GIVEN TaskId task = taskService.replace("foo", - new SpringBeanTask() { + new PersistentTask() { @Override public void accept(String state) { asserts.info(state); 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 fcf90639e..2f069aa71 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.time.temporal.ChronoUnit; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -11,11 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.annotation.Transactional; import org.sterl.spring.persistent_tasks.AbstractSpringTest; +import org.sterl.spring.persistent_tasks.api.PersistentTask; import org.sterl.spring.persistent_tasks.api.RetryStrategy; -import org.sterl.spring.persistent_tasks.api.RetryStrategy.MultiplicativeRetryStrategy; -import org.sterl.spring.persistent_tasks.api.SpringBeanTask; +import org.sterl.spring.persistent_tasks.api.TransactionalTask; 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; @@ -26,15 +24,29 @@ class SchedulerServiceTransactionTest extends AbstractSpringTest { private SchedulerService subject; private static AtomicBoolean sendError = new AtomicBoolean(false); - private static AtomicBoolean inTrx = new AtomicBoolean(false); @Autowired private PersonRepository personRepository; @Configuration static class Config { @Bean - SpringBeanTask savePerson(PersonRepository personRepository) { - return new SpringBeanTask<>() { - @Transactional + TransactionalTask savePersonInTrx(PersonRepository personRepository) { + return new TransactionalTask() { + @Override + public void accept(String name) { + personRepository.save(new PersonBE(name)); + if (sendError.get()) { + throw new RuntimeException("Error requested for " + name); + } + } + public RetryStrategy retryStrategy() { + return RetryStrategy.THREE_RETRIES_IMMEDIATELY; + } + }; + } + + @Bean + PersistentTask savePersonNoTrx(PersonRepository personRepository) { + return new PersistentTask<>() { @Override public void accept(String name) { personRepository.save(new PersonBE(name)); @@ -47,7 +59,7 @@ public RetryStrategy retryStrategy() { } @Override public boolean isTransactional() { - return inTrx.get(); + return false; } }; } @@ -59,29 +71,29 @@ public void beforeEach() throws Exception { subject = schedulerService; personRepository.deleteAllInBatch(); sendError.set(false); - inTrx.set(false); } - + @Test - void testSaveEntity() throws Exception { + void testSaveTransactions() throws Exception { // GIVEN - final var trigger = TaskTriggerBuilder.newTrigger("savePerson").state("Paul").build(); + final var request = TaskTriggerBuilder.newTrigger("savePersonNoTrx").state("Paul").build(); + var trigger = triggerService.queue(request); // WHEN hibernateAsserts.reset(); - subject.runOrQueue(trigger).get(); + triggerService.run(trigger); // THEN - // AND one the service, one the event and one more status update, - // one more to save the trigger - hibernateAsserts.assertTrxCount(4); + // AND one the service, one the event and one more status update + hibernateAsserts.assertTrxCount(3); assertThat(personRepository.count()).isOne(); } + @Test - void testSaveTransactions() throws Exception { + void testTrxCountTriggerService() throws Exception { // GIVEN - final var request = TaskTriggerBuilder.newTrigger("savePerson").state("Paul").build(); + final var request = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build(); var trigger = triggerService.queue(request); // WHEN @@ -89,33 +101,34 @@ void testSaveTransactions() throws Exception { triggerService.run(trigger); // THEN - // AND one the service, one the event and one more status update - hibernateAsserts.assertTrxCount(3); + hibernateAsserts.assertTrxCount(1); assertThat(personRepository.count()).isOne(); } - @Test - void testTrxCountTriggerService() throws Exception { + void testFailTrxCount() throws Exception { // GIVEN - final var request = TaskTriggerBuilder.newTrigger("savePerson").state("Paul").build(); + final var request = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build(); var trigger = triggerService.queue(request); - inTrx.set(true); + sendError.set(true); // WHEN hibernateAsserts.reset(); triggerService.run(trigger); // THEN - hibernateAsserts.assertTrxCount(1); - assertThat(personRepository.count()).isOne(); + // first the work which runs on error + // second the update to the trigger + // third to write the history + hibernateAsserts.assertTrxCount(3); } @Test void testRollbackAndRetry() throws Exception { // GIVEN - final var triggerRequest = TaskTriggerBuilder.newTrigger("savePerson").state("Paul").build(); + final var triggerRequest = TaskTriggerBuilder.newTrigger("savePersonInTrx").state("Paul").build(); sendError.set(true); + // WHEN var key = subject.runOrQueue(triggerRequest); // THEN diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskServiceTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskServiceTest.java index 10f9fc620..246249147 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskServiceTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskServiceTest.java @@ -2,27 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertThrows; -import java.util.ArrayList; - import org.junit.jupiter.api.Test; import org.sterl.spring.persistent_tasks.api.TaskId; -import org.sterl.spring.persistent_tasks.task.model.RegisteredTask; +import org.sterl.spring.persistent_tasks.task.component.TaskTransactionComponent; import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; class TaskServiceTest { - private final TaskService subject = new TaskService(new TaskRepository(new ArrayList<>())); + private final TaskService subject = new TaskService( + new TaskTransactionComponent(null, null), + new TaskRepository()); @Test void testAssertIsKnown() { // GIVEN - RegisteredTask t = new RegisteredTask<>("foo", (s) -> {}); - // WHEN - subject.replace(t); + var id = subject.replace("foo", (s) -> {}); // THEN - subject.assertIsKnown(t.getId()); + subject.assertIsKnown(id); subject.assertIsKnown(new TaskId("foo")); assertThrows(IllegalStateException.class, () -> subject.assertIsKnown(new TaskId("1"))); } diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java new file mode 100644 index 000000000..678fb8a47 --- /dev/null +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/task/TaskTransactionTest.java @@ -0,0 +1,149 @@ +package org.sterl.spring.persistent_tasks.task; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.sterl.spring.persistent_tasks.AbstractSpringTest; +import org.sterl.spring.persistent_tasks.api.PersistentTask; +import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder; +import org.sterl.spring.persistent_tasks.api.TransactionalTask; +import org.sterl.spring.persistent_tasks.task.util.ReflectionUtil; +import org.sterl.spring.sample_app.person.PersonBE; +import org.sterl.spring.sample_app.person.PersonRepository; + +import lombok.RequiredArgsConstructor; + +class TaskTransactionTest extends AbstractSpringTest { + + @Component("transactionalClass") + @Transactional(timeout = 5, propagation = Propagation.MANDATORY) + @RequiredArgsConstructor + static class TransactionalClass implements PersistentTask { + private final PersonRepository personRepository; + @Override + public void accept(String name) { + personRepository.save(new PersonBE(name)); + personRepository.save(new PersonBE(name)); + } + } + @Component("transactionalMethod") + @RequiredArgsConstructor + static class TransactionalMethod implements PersistentTask { + private final PersonRepository personRepository; + @Transactional(timeout = 6, propagation = Propagation.MANDATORY, isolation = Isolation.REPEATABLE_READ) + @Override + public void accept(String name) { + personRepository.save(new PersonBE(name)); + personRepository.save(new PersonBE(name)); + } + } + + /** + * A closure cannot be annotated, so we use a anonymous class + */ + @Configuration + static class Config { + @Bean("transactionalAnonymous") + PersistentTask transactionalAnonymous(PersonRepository personRepository) { + return new PersistentTask() { + @Transactional(timeout = 7, propagation = Propagation.REQUIRES_NEW) + @Override + public void accept(String name) { + personRepository.save(new PersonBE(name)); + } + }; + } + @Bean("transactionalClosure") + TransactionalTask transactionalClosure(PersonRepository personRepository) { + return name -> { + personRepository.save(new PersonBE(name)); + personRepository.save(new PersonBE(name)); + }; + } + } + + @Autowired TaskService subject; + @Autowired PersonRepository personRepository; + + @Autowired @Qualifier("transactionalClass") + PersistentTask transactionalClass; + @Autowired @Qualifier("transactionalMethod") + PersistentTask transactionalMethod; + @Autowired @Qualifier("transactionalAnonymous") + PersistentTask transactionalAnonymous; + + @Test + void testFindTransactionAnnotation() { + var a = ReflectionUtil.getAnnotation(transactionalClass, Transactional.class); + assertThat(a).isNotNull(); + assertThat(a.timeout()).isEqualTo(5); + + a = ReflectionUtil.getAnnotation(transactionalMethod, Transactional.class); + assertThat(a).isNotNull(); + assertThat(a.timeout()).isEqualTo(6); + + a = ReflectionUtil.getAnnotation(transactionalAnonymous, Transactional.class); + assertThat(a).isNotNull(); + assertThat(a.timeout()).isEqualTo(7); + } + + @Test + void testGetTransactionTemplate() { + var a = subject.getTransactionTemplate(transactionalClass); + assertThat(a).isPresent(); + assertThat(a.get().getTimeout()).isEqualTo(5); + assertThat(a.get().getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); + + a = subject.getTransactionTemplate(transactionalMethod); + assertThat(a).isPresent(); + assertThat(a.get().getTimeout()).isEqualTo(6); + assertThat(a.get().getPropagationBehavior()).isEqualTo(Propagation.REQUIRED.value()); + assertThat(a.get().getIsolationLevel()).isEqualTo(Isolation.REPEATABLE_READ.value()); + + a = subject.getTransactionTemplate(transactionalAnonymous); + assertThat(a).isEmpty(); + } + + @Test + void testRequiresNewHasOwnTransaction() { + // GIVEN + var t = triggerService.queue(TaskTriggerBuilder + .newTrigger("transactionalAnonymous", "test").build()); + + // WHEN + personRepository.deleteAllInBatch(); + hibernateAsserts.reset(); + triggerService.run(t).get(); + + // THEN + hibernateAsserts.assertTrxCount(3); + assertThat(personRepository.count()).isEqualTo(1); + } + + @ParameterizedTest + @ValueSource(strings = {"transactionalClass", "transactionalMethod", "transactionalClosure"}) + void testTransactionalTask(String task) { + // GIVEN + var t = triggerService.queue(TaskTriggerBuilder + .newTrigger(task, "test").build()); + + // WHEN + personRepository.deleteAllInBatch(); + hibernateAsserts.reset(); + triggerService.run(t).get(); + + // THEN + hibernateAsserts.assertTrxCount(1); + assertThat(personRepository.count()).isEqualTo(2); + } +} 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 b8f756996..3656ee70a 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 @@ -32,7 +32,7 @@ class TriggerServiceTest extends AbstractSpringTest { @Autowired private TaskRepository taskRepository; - // ensure task in the spring context + // ensure persistentTask in the spring context @Autowired private TaskId task1Id; @Autowired @@ -57,6 +57,10 @@ void testAddTrigger() throws Exception { final var triggerId = subject.queue(trigger).getKey(); // THEN + hibernateAsserts.assertTrxCount(1); + // one for the trigger and two for the history + hibernateAsserts.assertInsertCount(3); + // AND final var e = subject.get(triggerId); assertThat(e).isPresent(); assertThat(e.get().getData().getRunAt().toEpochSecond()).isEqualTo(triggerTime.toEpochSecond()); diff --git a/core/src/test/java/org/sterl/spring/sample_app/person/PersonBE.java b/core/src/test/java/org/sterl/spring/sample_app/person/PersonBE.java index e9a5eee39..4cd12c398 100644 --- a/core/src/test/java/org/sterl/spring/sample_app/person/PersonBE.java +++ b/core/src/test/java/org/sterl/spring/sample_app/person/PersonBE.java @@ -1,5 +1,7 @@ package org.sterl.spring.sample_app.person; +import java.io.Serializable; + import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -10,7 +12,7 @@ @Table @Entity @Data @NoArgsConstructor -public class PersonBE { +public class PersonBE implements Serializable { @Id @GeneratedValue diff --git a/db/pom.xml b/db/pom.xml index c8fff6174..3dee9ed43 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.3.2-SNAPSHOT + 1.4.0-SNAPSHOT ../pom.xml diff --git a/example/pom.xml b/example/pom.xml index 1b2293204..6ee9baf71 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.3.2-SNAPSHOT + 1.4.0-SNAPSHOT ../pom.xml 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 22f3cdb37..45c89aba3 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 @@ -28,6 +28,6 @@ public void accept(Vehicle vehicle) { } catch (InterruptedException e) { Thread.interrupted(); } - throw new RuntimeException("This task will always fail!"); + throw new RuntimeException("This persistentTask will always fail!"); } } diff --git a/pom.xml b/pom.xml index d19e32525..14797f16e 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.3.2-SNAPSHOT + 1.4.0-SNAPSHOT pom 2024 diff --git a/ui/pom.xml b/ui/pom.xml index f541831b8..468a1620f 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -6,7 +6,7 @@ org.sterl.spring spring-persistent-tasks-root - 1.3.2-SNAPSHOT + 1.4.0-SNAPSHOT ../pom.xml diff --git a/ui/src/main/java/org/sterl/spring/persistent_tasks_ui/SpringPersistentTasksUIConfig.java b/ui/src/main/java/org/sterl/spring/persistent_tasks_ui/SpringPersistentTasksUIConfig.java index cb40c7f1b..f60801e99 100644 --- a/ui/src/main/java/org/sterl/spring/persistent_tasks_ui/SpringPersistentTasksUIConfig.java +++ b/ui/src/main/java/org/sterl/spring/persistent_tasks_ui/SpringPersistentTasksUIConfig.java @@ -12,14 +12,14 @@ public class SpringPersistentTasksUIConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/task-ui").setViewName("/task-ui/index.html"); - //registry.addRedirectViewController("/task-ui/", "/task-ui"); + registry.addViewController("/persistentTask-ui").setViewName("/persistentTask-ui/index.html"); + //registry.addRedirectViewController("/persistentTask-ui/", "/persistentTask-ui"); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/task-ui/assets/**") - .addResourceLocations("classpath:/static/task-ui/assets/") + registry.addResourceHandler("/persistentTask-ui/assets/**") + .addResourceLocations("classpath:/static/persistentTask-ui/assets/") .setCacheControl(CacheControl.maxAge(90, TimeUnit.DAYS)); } } diff --git a/ui/src/server-api.d.ts b/ui/src/server-api.d.ts index 499c781e1..e04a567b7 100644 --- a/ui/src/server-api.d.ts +++ b/ui/src/server-api.d.ts @@ -34,6 +34,10 @@ export interface HistoryOverview { runningDurationInMs: number; } +export interface PersistentTask { + transactional: boolean; +} + export interface RetryStrategy { } @@ -43,12 +47,10 @@ export interface LinearRetryStrategy extends RetryStrategy { export interface MultiplicativeRetryStrategy extends RetryStrategy { } -export interface SpringBeanTask extends Consumer { - transactional: boolean; -} - -export interface Task extends SpringBeanTask { - id: TaskId; +/** + * @deprecated + */ +export interface SpringBeanTask extends PersistentTask { } export interface TaskId extends Serializable { @@ -58,6 +60,9 @@ export interface TaskId extends Serializable { export interface TaskTriggerBuilder { } +export interface TransactionalTask extends PersistentTask { +} + export interface Trigger { id: number; instanceId: number; @@ -98,7 +103,4 @@ export interface PageMetadata { export interface Serializable { } -export interface Consumer { -} - export type TriggerStatus = "WAITING" | "RUNNING" | "SUCCESS" | "FAILED" | "CANCELED";