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 0d0d655b7..9e45cf3f8 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 @@ -3,6 +3,8 @@ import java.io.Serializable; import java.time.Duration; import java.time.OffsetDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.github.f4b6a3.uuid.UuidCreator; @@ -13,6 +15,15 @@ * Represents the ID of a persistentTask, which is currently not running. */ public record TaskId(String name) implements Serializable { + + @SuppressWarnings("rawtypes") + private static final Map CACHE = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + public static TaskId of(String taskId) { + if (taskId == null || taskId.isBlank()) return null; + return CACHE.computeIfAbsent(taskId, s -> new TaskId<>(s)); + } public TriggerBuilder newTrigger() { return new TriggerBuilder<>(this); @@ -36,11 +47,6 @@ public TriggerRequest newUniqueTrigger(T state) { .build(); } - public static TaskId of(String taskId) { - if (taskId == null || taskId.isBlank()) return null; - return new TaskId<>(taskId); - } - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public static class TriggerBuilder { private final TaskId taskId; 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 52ea4199c..070cbe865 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 @@ -31,10 +31,11 @@ public static TriggerKey of(@Nullable String id, TaskId return new TriggerKey(id, taskId.name()); } - public TaskId toTaskId() { + public TaskId toTaskId() { if (taskName == null) return null; - return new TaskId<>(taskName); + return TaskId.of(taskName); } + /** * Builds a trigger for the given persistentTask name */ diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/JacksonStateSerializer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/JacksonStateSerializer.java new file mode 100644 index 000000000..b4a769cd5 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/JacksonStateSerializer.java @@ -0,0 +1,44 @@ +package org.sterl.spring.persistent_tasks.api.task; + +import java.io.Serializable; + +import org.springframework.lang.NonNull; +import org.sterl.spring.persistent_tasks.api.TaskId; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; + +/** + * Default implementation to use jackson instead java serialization for a task state. + * + * @param the type of the state for the deserialization + */ +@RequiredArgsConstructor +public class JacksonStateSerializer implements StateSerializer { + + private final ObjectMapper mapper; + private final Class clazz; + + @Override + public byte[] serialize(@NonNull TaskId id, @NonNull T obj) + throws SerializationFailedException{ + + try { + return mapper.writeValueAsBytes(obj); + } catch (JsonProcessingException e) { + throw new SerializationFailedException(obj, e); + } + } + + @Override + public T deserialize(@NonNull TaskId id, @NonNull byte[] bytes) + throws DeSerializationFailedException { + try { + return mapper.readValue(bytes, clazz); + } catch (Exception e) { + throw new DeSerializationFailedException(bytes, e); + } + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/SerializationProvider.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/SerializationProvider.java new file mode 100644 index 000000000..5fc2170ed --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/SerializationProvider.java @@ -0,0 +1,14 @@ +package org.sterl.spring.persistent_tasks.api.task; + +import java.io.Serializable; + +/** + * /** + * Any task may provide an own serialization class. + * + * @since 2.3.0 + * @param the type of the task state + */ +public interface SerializationProvider { + StateSerializer getSerializer(); +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/StateSerializer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/StateSerializer.java new file mode 100644 index 000000000..46dd356e2 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/api/task/StateSerializer.java @@ -0,0 +1,36 @@ +package org.sterl.spring.persistent_tasks.api.task; + +import java.io.Serializable; + +import org.springframework.lang.NonNull; +import org.sterl.spring.persistent_tasks.api.TaskId; +import org.sterl.spring.persistent_tasks.exception.SpringPersistentTaskException; + +/** + * Interface for a state serialization of task. + * + * @since 2.3.0 + * @param the state type + */ +public interface StateSerializer { + + byte[] serialize(@NonNull TaskId id, @NonNull T obj) throws SerializationFailedException; + + T deserialize(@NonNull TaskId id, @NonNull byte[] bytes) throws DeSerializationFailedException; + + 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); + } + } + + 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); + } + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/config/SpringPersistentTasksConfig.java b/core/src/main/java/org/sterl/spring/persistent_tasks/config/SpringPersistentTasksConfig.java index ff4294a08..d5e6393cf 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/config/SpringPersistentTasksConfig.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/config/SpringPersistentTasksConfig.java @@ -1,18 +1,36 @@ package org.sterl.spring.persistent_tasks.config; +import java.io.Serializable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.sterl.spring.persistent_tasks.EnableSpringPersistentTasks; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer; +import org.sterl.spring.persistent_tasks.trigger.component.DefaultStateSerializer; + +import lombok.extern.slf4j.Slf4j; @EnableScheduling @EnableAsync @AutoConfigurationPackage(basePackageClasses = EnableSpringPersistentTasks.class) @ComponentScan(basePackageClasses = EnableSpringPersistentTasks.class) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +@Configuration +@Slf4j public class SpringPersistentTasksConfig { + @Bean + @ConditionalOnMissingBean(DefaultStateSerializer.class) + StateSerializer defaultStateSerializer() { + log.info("Using java serialization for task states.", + DefaultStateSerializer.class.getSimpleName()); + return new DefaultStateSerializer(); + } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/FromCompletedTriggerEntityConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/FromCompletedTriggerEntityConverter.java new file mode 100644 index 000000000..ca202398f --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/FromCompletedTriggerEntityConverter.java @@ -0,0 +1,26 @@ +package org.sterl.spring.persistent_tasks.history.api; + +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.api.Trigger; +import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; +import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; +import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class FromCompletedTriggerEntityConverter implements ExtendetConvert { + + private final ToTrigger toTrigger; + + @NonNull + @Override + public Trigger convert(@NonNull CompletedTriggerEntity source) { + var result = toTrigger.convert(source); + result.setId(source.getId()); + result.setInstanceId(source.getId()); + return result; + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java index 851d60384..0017719f2 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java @@ -2,28 +2,12 @@ import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; -import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.HistoryTrigger; -import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity; import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; -import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; interface HistoryConverter { - enum FromLastTriggerStateEntity implements ExtendetConvert { - INSTANCE; - - @NonNull - @Override - public Trigger convert(@NonNull CompletedTriggerEntity source) { - var result = ToTrigger.INSTANCE.convert(source); - result.setId(source.getId()); - result.setInstanceId(source.getId()); - return result; - } - } - enum ToHistoryTrigger implements ExtendetConvert { INSTANCE; @@ -41,6 +25,5 @@ public HistoryTrigger convert(@NonNull HistoryTriggerEntity source) { result.setStatus(source.getStatus()); return result; } - } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java index 13983e14f..e6898083a 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java @@ -12,14 +12,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.sterl.spring.persistent_tasks.api.HistoryTrigger; import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.api.TriggerGroup; -import org.sterl.spring.persistent_tasks.api.HistoryTrigger; import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.history.HistoryService; -import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.FromLastTriggerStateEntity; import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.ToHistoryTrigger; import lombok.RequiredArgsConstructor; @@ -31,6 +30,7 @@ public class TriggerHistoryResource { public static final String PATH_GROUP = "history-grouped"; private final HistoryService historyService; + private final FromCompletedTriggerEntityConverter converter; @GetMapping("history/instance/{instanceId}") public PagedModel listInstances( @@ -56,7 +56,7 @@ public PagedModel list( TriggerSearch search, @PageableDefault(size = 100) Pageable page) { - return FromLastTriggerStateEntity.INSTANCE.toPage( // + return converter.toPage( // historyService.searchTriggers(search, page)); } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java index 541bc328c..2bbf050a7 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java @@ -1,19 +1,22 @@ package org.sterl.spring.persistent_tasks.shared.converter; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; import org.sterl.spring.persistent_tasks.api.Trigger; import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; import org.sterl.spring.persistent_tasks.shared.model.HasTrigger; import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; -import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer; +import org.sterl.spring.persistent_tasks.trigger.component.StateSerializationComponent; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -public enum ToTrigger implements ExtendetConvert { - INSTANCE; - - private final static StateSerializer SERIALIZER = new StateSerializer(); +@Component +@RequiredArgsConstructor +public class ToTrigger implements ExtendetConvert { + + private final StateSerializationComponent stateSerialization; @NonNull @Override @@ -33,7 +36,7 @@ public Trigger convert(@NonNull HasTrigger hasData) { result.setRunningDurationInMs(source.getRunningDurationInMs()); result.setStart(source.getStart()); try { - result.setState(SERIALIZER.deserialize(source.getState())); + result.setState(stateSerialization.deserialize(source)); } catch (Exception e) { var info = """ Failed to deserialize state 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 5c694b383..5217e5d2d 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 @@ -13,22 +13,43 @@ import org.springframework.transaction.support.TransactionTemplate; import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.task.PersistentTask; +import org.sterl.spring.persistent_tasks.api.task.SerializationProvider; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer; import org.sterl.spring.persistent_tasks.task.component.TaskTransactionComponent; import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor +@Slf4j public class TaskService { private final TaskTransactionComponent taskTransactionComponent; private final TaskRepository taskRepository; + + @SuppressWarnings("rawtypes") + private Map serializers = new ConcurrentHashMap<>(); + + // TODO move it to the component private final Map, Optional> cache = new ConcurrentHashMap<>(); public Set> findAllTaskIds() { return this.taskRepository.all(); } + + public void registerStateSerializer( + @NonNull TaskId id, + @NonNull StateSerializer serializer) { + log.info("{} brings an own state serializer: {}", id, serializer.getClass().getSimpleName()); + serializers.put(id, serializer); + } + @SuppressWarnings("unchecked") + public StateSerializer getStateSerializer(TaskId taskId, + StateSerializer fallBack) { + return serializers.getOrDefault(taskId, fallBack); + } public Optional> get(TaskId id) { return taskRepository.get(id); @@ -84,16 +105,25 @@ public TaskId register(String name, PersistentTask TaskId register(TaskId id, PersistentTask task) { taskTransactionComponent.buildOrGetDefaultTransactionTemplate(task); + if (task instanceof SerializationProvider sp) { + registerStateSerializer(id, sp.getSerializer()); + } return taskRepository.addTask(id, task); } /** * A way to manually register a PersistentTask, usually not needed as spring beans will be added automatically. */ - @SuppressWarnings("unchecked") public TaskId replace(String name, PersistentTask task) { - var id = (TaskId)TaskId.of(name); + return replace(TaskId.of(name), task); + } + + /** + * A way to manually register a PersistentTask, usually not needed as spring beans will be added automatically. + */ + public TaskId replace(TaskId id, PersistentTask task) { taskRepository.remove(id); return register(id, task); } 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 9f9163a68..b034add21 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 @@ -26,10 +26,9 @@ import org.sterl.spring.persistent_tasks.trigger.component.LockNextTriggerComponent; import org.sterl.spring.persistent_tasks.trigger.component.ReadTriggerComponent; import org.sterl.spring.persistent_tasks.trigger.component.RunTriggerComponent; -import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer; +import org.sterl.spring.persistent_tasks.trigger.component.StateSerializationComponent; import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; -import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -39,13 +38,12 @@ public class TriggerService { private final TaskService taskService; - @Getter - private final StateSerializer stateSerializer = new StateSerializer(); private final RunTriggerComponent runTrigger; private final ReadTriggerComponent readTrigger; private final EditTriggerComponent editTrigger; private final FailTriggerComponent failTrigger; private final LockNextTriggerComponent lockNextTrigger; + private final StateSerializationComponent stateSerialization; /** * Executes the given trigger directly in the current thread @@ -210,7 +208,7 @@ public List rescheduleAbandoned(OffsetDateTime timeout) { var now = OffsetDateTime.now().toEpochSecond(); result.forEach(t -> { final var task = taskService.get(t.newTaskId()); - final var state = stateSerializer.deserializeOrNull(t.getData().getState()); + final var state = stateSerialization.deserializeOrNull(t.key().toTaskId(), t.getData().getState()); final var e = new IllegalStateException("Trigger abandoned. Timeout: " + timeout + " running on: " + t.getRunningOn() diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/FromRunningTriggerEntityConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/FromRunningTriggerEntityConverter.java new file mode 100644 index 000000000..af325f524 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/FromRunningTriggerEntityConverter.java @@ -0,0 +1,27 @@ +package org.sterl.spring.persistent_tasks.trigger.api; + +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.api.Trigger; +import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; +import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; +import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class FromRunningTriggerEntityConverter implements ExtendetConvert { + + private final ToTrigger toTrigger; + + @Override + public Trigger convert(@NonNull RunningTriggerEntity source) { + var result = toTrigger.convert(source); + result.setId(source.getId()); + result.setInstanceId(source.getId()); + result.setRunningOn(source.getRunningOn()); + result.setLastPing(source.getLastPing()); + return result; + } +} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java deleted file mode 100644 index 3848f2e3b..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerConverter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sterl.spring.persistent_tasks.trigger.api; - -import org.springframework.lang.NonNull; -import org.sterl.spring.persistent_tasks.api.Trigger; -import org.sterl.spring.persistent_tasks.shared.ExtendetConvert; -import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger; -import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; - -public class TriggerConverter { - - public enum FromTriggerEntity implements ExtendetConvert { - INSTANCE; - - @Override - public Trigger convert(@NonNull RunningTriggerEntity source) { - var result = ToTrigger.INSTANCE.convert(source); - result.setId(source.getId()); - result.setInstanceId(source.getId()); - result.setRunningOn(source.getRunningOn()); - result.setLastPing(source.getLastPing()); - return result; - } - } -} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java index eba09022e..a49725a0a 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/api/TriggerResource.java @@ -19,7 +19,6 @@ import org.sterl.spring.persistent_tasks.api.TriggerKey; import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.trigger.TriggerService; -import org.sterl.spring.persistent_tasks.trigger.api.TriggerConverter.FromTriggerEntity; import lombok.RequiredArgsConstructor; @@ -30,6 +29,7 @@ public class TriggerResource { public static final String PATH_GROUPED = "triggers-grouped"; private final TriggerService triggerService; + private final FromRunningTriggerEntityConverter converter; @GetMapping("triggers/count") public long count() { @@ -49,7 +49,7 @@ public PagedModel list( TriggerSearch search, @PageableDefault(size = 100, direction = Direction.ASC, sort = "data.runAt") Pageable pageable) { - return FromTriggerEntity.INSTANCE.toPage( + return converter.toPage( triggerService.searchTriggers(search, pageable)); } @@ -60,7 +60,7 @@ public Optional setRunAt( @RequestBody OffsetDateTime runAt) { var result = triggerService.updateRunAt(new TriggerKey(id, taskName), runAt); - return FromTriggerEntity.INSTANCE.convert(result); + return converter.convert(result); } @DeleteMapping("triggers/{taskName}/{id}") @@ -68,7 +68,6 @@ public Optional cancelTrigger( @PathVariable("taskName") String taskName, @PathVariable("id") String id) { - return FromTriggerEntity.INSTANCE - .convert(triggerService.cancel(new TriggerKey(id, taskName))); + return converter.convert(triggerService.cancel(new TriggerKey(id, taskName))); } } diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/DefaultStateSerializer.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/DefaultStateSerializer.java new file mode 100644 index 000000000..68e034253 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/DefaultStateSerializer.java @@ -0,0 +1,57 @@ +package org.sterl.spring.persistent_tasks.trigger.component; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; + +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.core.serializer.Serializer; +import org.springframework.lang.NonNull; +import org.sterl.spring.persistent_tasks.api.TaskId; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class DefaultStateSerializer implements StateSerializer { + + private final Serializer serializer; + private final Deserializer deserializer; + + public DefaultStateSerializer() { + // needed for spring boot developer tools + // https://github.com/sterlp/spring-persistent-tasks/issues/19 + this(new DefaultSerializer(), + new DefaultDeserializer(Thread.currentThread().getContextClassLoader())); + } + + @SuppressWarnings("PMD.UnusedLocalVariable") + @Override + public byte[] serialize(@NonNull TaskId id, @NonNull Serializable obj) { + if (obj instanceof byte[] b) return b; + + try { + var bos = new ByteArrayOutputStream(512); + serializer.serialize(obj, bos); + return bos.toByteArray(); + } catch (IOException ex) { + throw new SerializationFailedException(obj, ex); + } + } + + @SuppressWarnings("PMD.UnusedLocalVariable") + @Override + public Serializable deserialize(@NonNull TaskId id, @NonNull byte[] bytes) { + try { + var bis = new ByteArrayInputStream(bytes); + return (Serializable)deserializer.deserialize(bis); + } catch (IOException ex) { + throw new DeSerializationFailedException(bytes, ex); + } + } +} 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 51ddd89c7..452778156 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 @@ -40,10 +40,10 @@ public class EditTriggerComponent { private final ApplicationEventPublisher publisher; - private final StateSerializer stateSerializer = new StateSerializer(); - private final ToTriggerData toTriggerData = new ToTriggerData(stateSerializer); + private final ToTriggerData toTriggerData; private final ReadTriggerComponent readTrigger; private final RunningTriggerRepository triggerRepository; + public Optional completeTaskWithSuccess(TriggerKey key, Serializable state) { final Optional result = readTrigger.get(key); @@ -101,7 +101,7 @@ private RunningTriggerEntity cancelTrigger(RunningTriggerEntity t, Exception e) publisher.publishEvent(new TriggerCanceledEvent( t.getId(), t.copyData(), - stateSerializer.deserializeOrNull(t.getData().getState()))); + toTriggerData.getStateSerialization().deserializeOrNull(t.getData()))); triggerRepository.delete(t); return t; @@ -164,14 +164,15 @@ public Optional resumeOne( TriggerSearch search, Function stateModifier) { search.setStatus(TriggerStatus.AWAITING_SIGNAL); - var foundTriggers = readTrigger.searchTriggers(search, Pageable.ofSize(1)); + final var foundTriggers = readTrigger.searchTriggers(search, Pageable.ofSize(1)); + final var stateSerializer = toTriggerData.getStateSerialization(); foundTriggers.forEach(t -> { log.debug("Resuming trigger={} with search={}", t, search); - var newStart = stateModifier.apply((T)stateSerializer.deserialize(t.getData().getState())); - t.getData().setState(stateSerializer.serialize(newStart)); + var newState = stateModifier.apply((T)stateSerializer.deserialize(t.getData())); + t.getData().setState(stateSerializer.serialize(t.newTaskId(), newState)); t.runAt(OffsetDateTime.now()); - publisher.publishEvent(new TriggerResumedEvent(t.getId(), t.copyData(), newStart)); + publisher.publishEvent(new TriggerResumedEvent(t.getId(), t.copyData(), newState)); }); return foundTriggers.isEmpty() ? Optional.empty() : Optional.of(foundTriggers.getContent().get(0)); @@ -184,8 +185,10 @@ public RunningTriggerEntity expireTrigger(RunningTriggerEntity t) { t.getData().updateRunningDuration(); publisher.publishEvent(new TriggerExpiredEvent( - t.getId(), t.copyData(), - stateSerializer.deserializeOrNull(t.getData().getState()))); + t.getId(), t.copyData(), + toTriggerData.getStateSerialization().deserializeOrNull(t.getData()) + ) + ); return t; } 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 59745e778..78703ae1d 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 @@ -23,7 +23,7 @@ public class RunTriggerComponent { private final TaskService taskService; private final FailTriggerComponent failTrigger; private final EditTriggerComponent editTrigger; - private final StateSerializer serializer = new StateSerializer(); + private final StateSerializationComponent stateSerialization; /** * Will execute the given {@link RunningTriggerEntity} and handle any errors @@ -62,7 +62,7 @@ private RunTaskWithStateCommand buildTaskWithStateFor(RunningTriggerEntity trigg try { final var task = taskService.assertIsKnown(trigger.newTaskId()); final var trx = taskService.getTransactionTemplateIfJoinable(task); - final var state = serializer.deserialize(trigger.getData().getState()); + final var state = stateSerialization.deserialize(trigger.getData()); return new RunTaskWithStateCommand(task, trx, state, trigger); } catch (Exception e) { failTrigger.execute(trigger, e); diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializationComponent.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializationComponent.java new file mode 100644 index 000000000..4907560c8 --- /dev/null +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializationComponent.java @@ -0,0 +1,79 @@ +package org.sterl.spring.persistent_tasks.trigger.component; + +import java.io.Serializable; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.api.TaskId; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer.DeSerializationFailedException; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer.SerializationFailedException; +import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; +import org.sterl.spring.persistent_tasks.task.TaskService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StateSerializationComponent { + + private final StateSerializer defaultStateSerializer; + private final TaskService taskService; + + @Nullable + public byte[] serialize(@NonNull TaskId id, @Nullable final T obj) { + if (obj == null) return null; + StateSerializer d = taskService.getStateSerializer(id, defaultStateSerializer); + try { + return d.serialize(id, obj); + } catch (SerializationFailedException e) { + throw e; + } catch (Exception ex) { + throw new SerializationFailedException(obj, ex); + } + } + + @Nullable + public T deserialize(@NonNull TaskId id, @Nullable byte[] bytes) { + if (bytes == null) return null; + + StateSerializer d = taskService.getStateSerializer(id, defaultStateSerializer); + try { + return d.deserialize(id, bytes); + } catch (DeSerializationFailedException e) { + throw e; + } catch (Exception ex) { + throw new DeSerializationFailedException(bytes, ex); + } + } + + @Nullable + public Serializable deserialize(TriggerEntity data) { + if (data == null) return null; + return deserialize(data.getKey().toTaskId(), data.getState()); + } + + @Nullable + public T deserializeOrNull(TaskId id, byte[] bytes) { + try { + return deserialize(id, bytes); + } catch (DeSerializationFailedException e) { + log.warn("Failed to deserialize state of {} - returning null.", id, e); + return null; + } + } + + @Nullable + public Serializable deserializeOrNull(TriggerEntity data) { + if (data == null) return null; + try { + return deserialize(data); + } catch (Exception e) { + log.warn("Failed to deserialize state of {} - returning null.", data.getKey(), e); + return null; + } + } +} 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 deleted file mode 100644 index f2d7940fb..000000000 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/StateSerializer.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.sterl.spring.persistent_tasks.trigger.component; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamClass; -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) { - return null; - } - if (obj instanceof byte[] b) return b; - - var bos = new ByteArrayOutputStream(512); - try (var out = new ObjectOutputStream(bos)) { - out.writeObject(obj); - return bos.toByteArray(); - } catch (Exception ex) { - throw new SerializationFailedException(obj, ex); - } - } - - public Serializable deserialize(byte[] bytes) { - if (bytes == null) { - return null; - } - - var bis = new ByteArrayInputStream(bytes); - try (var in = new ContextClassLoaderObjectInputStream(bis)) { - return (Serializable)in.readObject(); - } catch (Exception ex) { - throw new DeSerializationFailedException(bytes, ex); - } - } - - public Serializable deserializeOrNull(byte[] bytes) { - try { - return deserialize(bytes); - } catch (Exception e) { - log.error("Failed to deserialize bytes", e); - return null; - } - } - - // needed for spring boot developer tools - // https://github.com/sterlp/spring-persistent-tasks/issues/19 - static class ContextClassLoaderObjectInputStream extends ObjectInputStream { - ContextClassLoaderObjectInputStream(InputStream in) throws IOException { - super(in); - } - @Override - protected Class resolveClass(ObjectStreamClass desc) - throws IOException, ClassNotFoundException { - return Class.forName(desc.getName(), false, - Thread.currentThread().getContextClassLoader()); - } - } -} diff --git a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java index d3f56ea08..385d2ce01 100644 --- a/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java +++ b/core/src/main/java/org/sterl/spring/persistent_tasks/trigger/component/ToTriggerData.java @@ -1,30 +1,39 @@ package org.sterl.spring.persistent_tasks.trigger.component; +import java.io.Serializable; + import org.springframework.core.convert.converter.Converter; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.sterl.spring.persistent_tasks.api.TaskId; import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerStatus; import org.sterl.spring.persistent_tasks.api.task.RunningTriggerContextHolder; import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity; +import lombok.Getter; import lombok.RequiredArgsConstructor; +@Component @RequiredArgsConstructor -public class ToTriggerData implements Converter, TriggerEntity> { +public class ToTriggerData implements Converter, TriggerEntity> { - private final StateSerializer stateSerializer; + @Getter + private final StateSerializationComponent stateSerialization; @Override @Nullable - public TriggerEntity convert(@NonNull TriggerRequest trigger) { + public TriggerEntity convert(@NonNull TriggerRequest trigger) { var correlationId = RunningTriggerContextHolder.buildOrGetCorrelationId(trigger.correlationId()); - byte[] state = stateSerializer.serialize(trigger.state()); + TaskId taskId = trigger.taskId(); + Serializable state = trigger.state(); + byte[] stateBytes = stateSerialization.serialize(taskId, state); final var data = TriggerEntity.builder() .key(trigger.key()) .runAt(trigger.runtAt()) .priority(trigger.priority()) - .state(state) + .state(stateBytes) .status(trigger.status() == null ? TriggerStatus.WAITING : trigger.status()) .correlationId(correlationId) .tag(trigger.tag()); 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 5cd7518a1..d612a7d31 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 @@ -34,6 +34,7 @@ import org.sterl.spring.persistent_tasks.test.AsyncAsserts; import org.sterl.spring.persistent_tasks.test.PersistentTaskTestService; import org.sterl.spring.persistent_tasks.trigger.TriggerService; +import org.sterl.spring.persistent_tasks.trigger.component.StateSerializationComponent; import org.sterl.spring.persistent_tasks.trigger.model.RunningTriggerEntity; import org.sterl.spring.sample_app.SampleApp; import org.sterl.test.hibernate_asserts.HibernateAsserts; @@ -60,6 +61,9 @@ public class AbstractSpringTest { @Qualifier("schedulerB") protected SchedulerService schedulerB; + @Autowired + protected StateSerializationComponent stateSerialization; + @Autowired protected TriggerService triggerService; @Autowired 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 246249147..53a399a86 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 @@ -1,17 +1,94 @@ package org.sterl.spring.persistent_tasks.task; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; +import java.time.OffsetDateTime; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.sterl.spring.persistent_tasks.AbstractSpringTest; import org.sterl.spring.persistent_tasks.api.TaskId; -import org.sterl.spring.persistent_tasks.task.component.TaskTransactionComponent; -import org.sterl.spring.persistent_tasks.task.repository.TaskRepository; +import org.sterl.spring.persistent_tasks.api.task.JacksonStateSerializer; +import org.sterl.spring.persistent_tasks.api.task.PersistentTask; +import org.sterl.spring.persistent_tasks.api.task.SerializationProvider; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer; +import org.sterl.spring.persistent_tasks.test.AsyncAsserts; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; + +class TaskServiceTest extends AbstractSpringTest { + + public record TaskState(int id, String message) implements Serializable {} + + public static class LoggingJacksonStateSerializer + extends JacksonStateSerializer { + + private final AtomicInteger deserializeCount = new AtomicInteger(0); + private final AtomicInteger serializeCount = new AtomicInteger(0); + private final AsyncAsserts asserts; + + public LoggingJacksonStateSerializer(AsyncAsserts asserts, ObjectMapper mapper, Class clazz) { + super(mapper, clazz); + this.asserts = asserts; + } + + @Override + public T deserialize(@NonNull TaskId id, @NonNull byte[] bytes) throws DeSerializationFailedException { + Objects.requireNonNull(bytes, "bytes are null!"); + Objects.requireNonNull(id, "TaskId is null"); + deserializeCount.incrementAndGet(); + asserts.add("deserialize " + id.name()); + return super.deserialize(id, bytes); + } + + @Override + public byte[] serialize(@NonNull TaskId id, @NonNull T obj) throws SerializationFailedException { + Objects.requireNonNull(obj, "T is null!"); + Objects.requireNonNull(id, "TaskId is null"); + serializeCount.incrementAndGet(); + asserts.add("serialize " + id.name()); + return super.serialize(id, obj); + } + + public int getDeserializeCount() { + return deserializeCount.get(); + } + public int getSerializeCount() { + return serializeCount.get(); + } + } + + @RequiredArgsConstructor + public static class TaskWithOwnSerialization + implements PersistentTask, SerializationProvider { + + private final StateSerializer stateSerializer; + private final AsyncAsserts asserts; -class TaskServiceTest { + @Override + public StateSerializer getSerializer() { + return stateSerializer; + } - private final TaskService subject = new TaskService( - new TaskTransactionComponent(null, null), - new TaskRepository()); + @Override + public void accept(@Nullable TaskState state) { + asserts.info("state: " + state); + } + } + + @Autowired + private ObjectMapper mapper; + + @Autowired + private TaskService subject; @Test void testAssertIsKnown() { @@ -24,4 +101,54 @@ void testAssertIsKnown() { subject.assertIsKnown(new TaskId("foo")); assertThrows(IllegalStateException.class, () -> subject.assertIsKnown(new TaskId("1"))); } + + @Test + void testStateSerializerisRegistered() throws Exception { + // GIVEN + TaskId taskId = TaskId.of("testStateSerializerisRegistered"); + // AND + var stateSerializer = subject.getStateSerializer(taskId, null); + assertThat(stateSerializer).isNull(); + // AND + stateSerializer = subject.getStateSerializer(taskId, null); + taskService.replace(taskId, + new TaskWithOwnSerialization(new JacksonStateSerializer<>(mapper, TaskState.class), asserts)); + + // WHEN + stateSerializer = subject.getStateSerializer(taskId, null); + + // THEN + assertThat(stateSerializer).isNotNull(); + } + + @Test + void testUsesTaskSerialization() throws Exception { + // GIVEN + var customSerialization = new LoggingJacksonStateSerializer<>(asserts, mapper, TaskState.class); + TaskId taskId1 = TaskId.of("testUsesTaskSerialization1"); + taskService.replace(taskId1, new TaskWithOwnSerialization(customSerialization, asserts)); + TaskId task2 = taskService.replace("testUsesTaskSerialization2", s -> { + asserts.info("task2::" + s); + }); + + // WHEN + triggerService.queue(taskId1.newTrigger().build()); + triggerService.queue(task2.newTrigger().state("Hallo task2").build()); + + // THEN + assertThat(customSerialization.getDeserializeCount()).isZero(); + assertThat(customSerialization.getSerializeCount()).isZero(); + + // WHEN + triggerService.queue(taskId1.newTrigger().state(new TaskState(1, "hallo task1")) .build()); + // THEN + assertThat(customSerialization.getDeserializeCount()).isZero(); + assertThat(customSerialization.getSerializeCount()).isOne(); + + // WHEN + persistentTaskTestService.runAllDueTrigger(OffsetDateTime.now()); + // THEN + assertThat(customSerialization.getDeserializeCount()).isOne(); + assertThat(customSerialization.getSerializeCount()).isOne(); + } } 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 7f7204c6b..c08c657cb 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 @@ -22,10 +22,10 @@ import org.sterl.spring.persistent_tasks.api.TriggerRequest; import org.sterl.spring.persistent_tasks.api.TriggerSearch; import org.sterl.spring.persistent_tasks.api.TriggerStatus; +import org.sterl.spring.persistent_tasks.api.task.StateSerializer.DeSerializationFailedException; import org.sterl.spring.persistent_tasks.history.repository.CompletedTriggerRepository; import org.sterl.spring.persistent_tasks.task.exception.CancelTaskException; import org.sterl.spring.persistent_tasks.task.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; @@ -114,7 +114,7 @@ void testCreateTrigger() { // AND assertThat(events.stream(TriggerAddedEvent.class).count()).isEqualTo(2); var t = subject.get(t2.getKey()).get(); - assertThat(t.getData().getState()).isEqualTo(subject.getStateSerializer().serialize("foo")); + assertThat(t.getData().getState()).isEqualTo(stateSerialization.serialize(taskId, "foo")); } @Test @@ -545,7 +545,7 @@ void testResumeWaitingTriggerForSignal() { // THEN var t = subject.get(triggerKey).get(); assertThat(t.getData().getStatus()).isEqualTo(TriggerStatus.WAITING); - assertThat(t.getData().getState()).isEqualTo(subject.getStateSerializer().serialize("new state")); + assertThat(t.getData().getState()).isEqualTo(stateSerialization.serialize(taskId, "new state")); // WHEN assertThat(persistentTaskTestService.runNextTrigger()).isPresent(); @@ -588,7 +588,7 @@ void testResumeWaitingTriggerWithFunction() { // THEN var t = subject.get(triggerKey).get(); assertThat(t.getData().getStatus()).isEqualTo(TriggerStatus.WAITING); - assertThat(t.getData().getState()).isEqualTo(subject.getStateSerializer().serialize("Cool new State")); + assertThat(t.getData().getState()).isEqualTo(stateSerialization.serialize(taskId, "Cool new State")); // WHEN assertThat(persistentTaskTestService.runNextTrigger()).isPresent(); diff --git a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponentTest.java b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponentTest.java index 8b181b34d..1dc116f9a 100644 --- a/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponentTest.java +++ b/core/src/test/java/org/sterl/spring/persistent_tasks/trigger/component/ReadTriggerComponentTest.java @@ -11,7 +11,7 @@ class ReadTriggerComponentTest extends AbstractSpringTest { @Autowired - ReadTriggerComponent subject; + private ReadTriggerComponent subject; @Test void test() { diff --git a/doc/docs/.nav.yml b/doc/docs/.nav.yml new file mode 100644 index 000000000..9ef0b883c --- /dev/null +++ b/doc/docs/.nav.yml @@ -0,0 +1,5 @@ +nav: + - Home: "index.md" + - Register a Task: "register-spring-task.md" + - "*" + - JavaDoc: https://sterlp.github.io/spring-persistent-tasks/javadoc-core \ No newline at end of file diff --git a/doc/docs/delete-task-trigger.md b/doc/docs/delete-task-trigger.md index c9a1f3997..dc579e23a 100644 --- a/doc/docs/delete-task-trigger.md +++ b/doc/docs/delete-task-trigger.md @@ -22,7 +22,7 @@ void testCancelTrigger() { ## Cancel a running trigger As soon a task is triggered, a task may decide to cancel or to fail itself. Both will suppress any outstanding retries of the retry strategy. -![Task Exceprtion](./assets/spring-persistent-task-exception.png){ align=center } +![Task Exceprtion](/assets/spring-persistent-task-exception.png){ align=center } ```java @Test diff --git a/doc/docs/index.md b/doc/docs/index.md index 3d742dd0c..272c55a6c 100644 --- a/doc/docs/index.md +++ b/doc/docs/index.md @@ -6,7 +6,7 @@ A simple task management framework designed to queue and execute asynchronous ta Focus is the usage with spring boot and JPA. -![Dashboard](./assets/dashboard.png) +![Dashboard](/assets/dashboard.png) ## Key Features ✨ diff --git a/doc/docs/life-cycle-events.md b/doc/docs/life-cycle-events.md index fcacf0557..6f35f7761 100644 --- a/doc/docs/life-cycle-events.md +++ b/doc/docs/life-cycle-events.md @@ -8,7 +8,7 @@ Any trigger follows a particular life cycle having the status: 1. FAILED => TriggerFailedEvent 1. CANCELED => TriggerCanceledEvent -![TriggerLifeCycleEvent](./assets/trigger-life-cycle-events.png) +![TriggerLifeCycleEvent](/assets/trigger-life-cycle-events.png) # Create a custom life cycle listener diff --git a/doc/docs/register-spring-task.md b/doc/docs/register-spring-task.md index 2a283836e..f3ece3796 100644 --- a/doc/docs/register-spring-task.md +++ b/doc/docs/register-spring-task.md @@ -1,6 +1,6 @@ # Register a Tasks -![Spring Persistent Task Interface](assets/spring-persistent-task-interface.png) +![Spring Persistent Task Interface](/assets/spring-persistent-task-interface.png) ## RunningTriggerContextHolder @since v1.6 diff --git a/doc/docs/csrf-daschboard-ui.md b/doc/docs/setup/csrf-daschboard-ui.md similarity index 100% rename from doc/docs/csrf-daschboard-ui.md rename to doc/docs/setup/csrf-daschboard-ui.md diff --git a/doc/docs/setup/cusomize-serialization.md b/doc/docs/setup/cusomize-serialization.md new file mode 100644 index 000000000..8abb67f05 --- /dev/null +++ b/doc/docs/setup/cusomize-serialization.md @@ -0,0 +1,53 @@ +By default java serialization is used to serializer the state. It can by customized in a task or for all tasks. + +## Use Jackson serialization for a task @since v2.3 + +```java +public record TaskState(int id, String message) implements Serializable {} + +@RequiredArgsConstructor +public class TaskWithOwnSerialization + implements PersistentTask, SerializationProvider { + + private final ObjectMapper mapper; + private final AsyncAsserts asserts; + + @Override + public StateSerializer getSerializer() { + return new JacksonStateSerializer<>(mapper, TaskState.class); + } + + @Override + public void accept(@Nullable TaskState state) { + asserts.info("state: " + state); + } +} +``` + +## Replace the default serialization @since v2.3 + +By default the `DefaultStateSerializer` uses Java Serialization and can be replaced if required: + +```java +@Bean +@ConditionalOnMissingBean(DefaultStateSerializer.class) +StateSerializer defaultStateSerializer() { + log.info("Using java serialization for task states.", + DefaultStateSerializer.class.getSimpleName()); + return new DefaultStateSerializer(); +} +``` + +For compatibility with Spring DEV tools the current thread is registered during startup as class loader context: + +```java +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.core.serializer.Serializer; + +public DefaultStateSerializer() { + this.serializer = new DefaultSerializer(); + this.deserializer = new DefaultDeserializer(Thread.currentThread().getContextClassLoader()); +} +``` diff --git a/doc/docs/liquibase-setup.md b/doc/docs/setup/liquibase-setup.md similarity index 100% rename from doc/docs/liquibase-setup.md rename to doc/docs/setup/liquibase-setup.md diff --git a/doc/docs/maven-setup.md b/doc/docs/setup/maven-setup.md similarity index 100% rename from doc/docs/maven-setup.md rename to doc/docs/setup/maven-setup.md diff --git a/doc/docs/scheduler-name.md b/doc/docs/setup/scheduler-name.md similarity index 100% rename from doc/docs/scheduler-name.md rename to doc/docs/setup/scheduler-name.md diff --git a/doc/docs/spring-configuration-options.md b/doc/docs/setup/spring-configuration-options.md similarity index 100% rename from doc/docs/spring-configuration-options.md rename to doc/docs/setup/spring-configuration-options.md diff --git a/doc/docs/junit-test.md b/doc/docs/test/junit-test.md similarity index 99% rename from doc/docs/junit-test.md rename to doc/docs/test/junit-test.md index 86bf07b4a..2d54874e6 100644 --- a/doc/docs/junit-test.md +++ b/doc/docs/test/junit-test.md @@ -5,7 +5,7 @@ The `SchedulerService` can be disabled for unit testing, which ensures that no trigger will be executed automatically. -```yml +```yaml spring: persistent-tasks: scheduler-enabled: false diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index 64822bdaf..035e2b8a7 100644 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -1,14 +1,22 @@ site_name: Spring Persistent Tasks site_url: https://spring-persistent-task.sterl.org +use_directory_urls: false theme: name: material features: - content.code.copy - content.code.annotate + - navigation.indexes + - navigation.path + - navigation.prune + - navigation.expand + plugins: - search - tags - glightbox + - awesome-nav + markdown_extensions: - attr_list - md_in_html diff --git a/doc/requirements.txt b/doc/requirements.txt index 1ef3a04b7..573a9863e 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,3 @@ mkdocs-material -mkdocs-glightbox \ No newline at end of file +mkdocs-glightbox +mkdocs-awesome-pages-plugin \ No newline at end of file diff --git a/example/pom.xml b/example/pom.xml index 11b756a74..70c0c1068 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -17,7 +17,7 @@ - 2.2.4-SNAPSHOT + 2.2.5-SNAPSHOT