diff --git a/README.md b/README.md index 8c8126b7..33f0a979 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ scheduler.schedule(myAdhocTask.instance("1045", new MyTaskData(1001L)), Instant. ### More examples +#### Plain Java + * [EnableImmediateExecutionMain.java](./examples/features/src/main/java/com/github/kagkarlsson/examples/EnableImmediateExecutionMain.java) * [MaxRetriesMain.java](./examples/features/src/main/java/com/github/kagkarlsson/examples/MaxRetriesMain.java) * [ExponentialBackoffMain.java](./examples/features/src/main/java/com/github/kagkarlsson/examples/ExponentialBackoffMain.java) @@ -159,6 +161,21 @@ scheduler.schedule(myAdhocTask.instance("1045", new MyTaskData(1001L)), Instant. * [JobChainingUsingTaskDataMain.java](./examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingTaskDataMain.java) * [JobChainingUsingSeparateTasksMain.java](./examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingSeparateTasksMain.java) +#### Spring Boot + + + +| Example | Description | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [BasicExamples](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/BasicExamplesConfiguration.java) | A basic one-time task and recurring task | +| [TransactionallyStagedJob](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TransactionallyStagedJobConfiguration.java) | Example of [transactionally staging a job](https://brandur.org/job-drain), i.e. making sure the background job runs **iff** the transaction commits (along with other db-modifications). | +| [LongRunningJob](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/LongRunningJobConfiguration.java) | Long-running jobs need to **survive application restarts** and avoid restarting from the beginning. This example demonstrates how to **persisting progress** on shutdown and additionally a technique for limiting the job to run nightly. | +| [RecurringStateTracking](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/RecurringStateTrackingConfiguration.java) | A recurring task with state that can be modified after each run. | +| [ParallellJobSpawner](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/ParallellJobConfiguration.java) | Demonstrates how to use a recurring job to spawn one-time jobs, e.g. for parallelization. | +| [JobChaining](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/JobChainingConfiguration.java) | A one-time job with **multiple steps**. The next step is scheduled after the previous one completes. | +| [MultiInstanceRecurring](./examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/MultiInstanceRecurringConfiguration.java) | Demonstrates how to achieve **multiple recurring jobs** of the same type, but potentially differing schedules and data. | + + ## Configuration diff --git a/db-scheduler/pom.xml b/db-scheduler/pom.xml index 73404e7e..6c35eb6e 100644 --- a/db-scheduler/pom.xml +++ b/db-scheduler/pom.xml @@ -76,6 +76,11 @@ jackson-annotations true + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + true + diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java index ffa9f63c..a8966857 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java @@ -37,10 +37,13 @@ public Instant getExecutionTime() { @SuppressWarnings("unchecked") public DATA_TYPE getData() { - if (dataClass.isInstance(this.execution.taskInstance.getData())) { - return (DATA_TYPE) this.execution.taskInstance.getData(); + Object data = this.execution.taskInstance.getData(); + if (data == null) { + return null; + } else if (dataClass.isInstance(data)) { + return (DATA_TYPE) data; } - throw new DataClassMismatchException(dataClass, this.execution.taskInstance.getData().getClass()); + throw new DataClassMismatchException(dataClass, data.getClass()); } public Instant getLastSuccess() { diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/GsonSerializer.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/GsonSerializer.java index 234d55ed..428574cb 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/GsonSerializer.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/GsonSerializer.java @@ -15,13 +15,20 @@ */ package com.github.kagkarlsson.scheduler.serializer; -import com.github.kagkarlsson.scheduler.serializer.gson.InstantAdapter; +import com.github.kagkarlsson.scheduler.serializer.gson.*; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; +import com.github.kagkarlsson.scheduler.task.schedule.Daily; +import com.github.kagkarlsson.scheduler.task.schedule.FixedDelay; +import com.github.kagkarlsson.scheduler.task.schedule.Schedule; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.function.Consumer; public class GsonSerializer implements Serializer { @@ -29,9 +36,19 @@ public class GsonSerializer implements Serializer { private final Gson gson; public static GsonBuilder getDefaultGson() { + RuntimeTypeAdapterFactory runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory + .of(Schedule.class, "type") + .registerSubtype(CronSchedule.class, "cron") + .registerSubtype(FixedDelay.class, "fixedDelay") + .registerSubtype(Daily.class, "daily"); + return new GsonBuilder() .serializeNulls() - .registerTypeAdapter(Instant.class, new InstantAdapter()); + .registerTypeAdapter(Instant.class, new InstantAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()) + .registerTypeHierarchyAdapter(ZoneId.class, new ZoneIdAdapter()) + .registerTypeAdapterFactory(runtimeTypeAdapterFactory); } public GsonSerializer() { diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/JacksonSerializer.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/JacksonSerializer.java index 99f7c9c1..208badb2 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/JacksonSerializer.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/JacksonSerializer.java @@ -19,19 +19,17 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.github.kagkarlsson.scheduler.exceptions.SerializationException; -import com.github.kagkarlsson.scheduler.serializer.gson.InstantAdapter; +import com.github.kagkarlsson.scheduler.serializer.jackson.ScheduleMixin; import com.github.kagkarlsson.scheduler.serializer.jackson.InstantDeserializer; import com.github.kagkarlsson.scheduler.serializer.jackson.InstantSerializer; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; +import com.github.kagkarlsson.scheduler.task.schedule.Schedule; import java.io.IOException; -import java.sql.Time; import java.time.Instant; import java.util.function.Consumer; @@ -46,7 +44,9 @@ public static ObjectMapper getDefaultObjectMapper() { return new ObjectMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - .registerModule(module); + .addMixIn(Schedule.class, ScheduleMixin.class) + .registerModule(module) + .registerModule(new JavaTimeModule()); } public JacksonSerializer() { diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/DurationAdapter.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/DurationAdapter.java new file mode 100644 index 00000000..4fdd156c --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/DurationAdapter.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.serializer.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +public class DurationAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter jsonWriter, Duration duration) throws IOException { + jsonWriter.value(duration.toMillis()); + } + + @Override + public Duration read(JsonReader jsonReader) throws IOException { + return Duration.ofMillis(Long.parseLong(jsonReader.nextString())); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/LocalTimeAdapter.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/LocalTimeAdapter.java new file mode 100644 index 00000000..9b773bf4 --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/LocalTimeAdapter.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.serializer.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +public class LocalTimeAdapter extends TypeAdapter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); + + @Override + public void write(JsonWriter jsonWriter, LocalTime localTime) throws IOException { + jsonWriter.value(localTime.format(FORMATTER)); + } + + @Override + public LocalTime read(JsonReader jsonReader) throws IOException { + return FORMATTER.parse(jsonReader.nextString(), LocalTime::from); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/RuntimeTypeAdapterFactory.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/RuntimeTypeAdapterFactory.java new file mode 100644 index 00000000..ffa6a173 --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/RuntimeTypeAdapterFactory.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.serializer.gson; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap<>(); + private final Map, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + private boolean recognizeSubtypes; + + private RuntimeTypeAdapterFactory( + Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * + * @param maintainType true if the type field should be included in deserialized objects + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /** + * Ensures that this factory will handle not just the given {@code baseType}, but any subtype + * of that type. + */ + public RuntimeTypeAdapterFactory recognizeSubtypes() { + this.recognizeSubtypes = true; + return this; + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + Class rawType = type.getRawType(); + boolean handle = + recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); + if (!handle) { + return null; + } + + final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + final Map> labelToDelegate = new LinkedHashMap<>(); + final Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = jsonElementAdapter.read(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/ZoneIdAdapter.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/ZoneIdAdapter.java new file mode 100644 index 00000000..1d41ce35 --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/gson/ZoneIdAdapter.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.serializer.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.ZoneId; + +public class ZoneIdAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter jsonWriter, ZoneId zoneId) throws IOException { + jsonWriter.value(zoneId.getId()); + } + + @Override + public ZoneId read(JsonReader jsonReader) throws IOException { + return ZoneId.of(jsonReader.nextString()); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleMixin.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleMixin.java new file mode 100644 index 00000000..844bec3a --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleMixin.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.serializer.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; +import com.github.kagkarlsson.scheduler.task.schedule.Daily; +import com.github.kagkarlsson.scheduler.task.schedule.FixedDelay; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = CronSchedule.class, name = "cron"), + @JsonSubTypes.Type(value = FixedDelay.class, name = "fixedDelay"), + @JsonSubTypes.Type(value = Daily.class, name = "daily") +}) +public abstract class ScheduleMixin { + @JsonIgnore + abstract boolean isDeterministic(); + @JsonIgnore + abstract boolean isDisabled(); +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/CompletionHandler.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/CompletionHandler.java index a92bba59..ca113f4b 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/CompletionHandler.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/CompletionHandler.java @@ -15,12 +15,12 @@ */ package com.github.kagkarlsson.scheduler.task; -import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; import com.github.kagkarlsson.scheduler.task.schedule.Schedule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; +import java.util.function.Function; public interface CompletionHandler { @@ -70,4 +70,47 @@ public String toString() { } + class OnCompleteReplace implements CompletionHandler { + private static final Logger LOG = LoggerFactory.getLogger(OnCompleteReplace.class); + private String newTaskName = ""; // used for logging purposes only + private final Function, SchedulableInstance> newInstanceCreator; + + public OnCompleteReplace(String newTaskName) { + this(newTaskName, null); + } + + public OnCompleteReplace(String newTaskName, T newData) { + this((TaskInstance currentInstance) -> { + return SchedulableInstance.of(new TaskInstance<>(newTaskName, currentInstance.getId(), newData), Instant.now()); + }); + this.newTaskName = newTaskName; + } + + public OnCompleteReplace(TaskDescriptor newTask, T newData) { + this((TaskInstance currentInstance) -> { + return SchedulableInstance.of(new TaskInstance<>(newTask.getTaskName(), currentInstance.getId(), newData), Instant.now()); + }); + this.newTaskName = newTask.getTaskName(); + } + + public OnCompleteReplace(Function, SchedulableInstance> newInstanceCreator) { + this.newInstanceCreator = newInstanceCreator; + } + + + @Override + @SuppressWarnings({"unchecked"}) + public void complete(ExecutionComplete executionComplete, ExecutionOperations executionOperations) { + TaskInstance currentInstance = executionComplete.getExecution().taskInstance; + SchedulableInstance nextInstance = newInstanceCreator.apply(currentInstance); + LOG.debug("Removing instance {} and scheduling instance {}", executionComplete.getExecution().taskInstance, nextInstance); + executionOperations.removeAndScheduleNew(nextInstance); + } + + @Override + public String toString() { + return "OnCompleteReplace with task '"+newTaskName+"'"; + } + + } } diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/HasTaskName.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/HasTaskName.java new file mode 100644 index 00000000..c724095c --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/HasTaskName.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.task; + +public interface HasTaskName { + String getTaskName(); + + static HasTaskName of(String name) { + return new SimpleTaskName(name); + } + + class SimpleTaskName implements HasTaskName { + private String name; + + public SimpleTaskName(String name) { + + this.name = name; + } + + @Override + public String getTaskName() { + return name; + } + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/Task.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/Task.java index acbe9182..cd9a29f9 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/Task.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/Task.java @@ -17,7 +17,7 @@ import java.time.Instant; -public interface Task extends ExecutionHandler { +public interface Task extends ExecutionHandler, HasTaskName { String getName(); Class getDataClass(); @@ -33,4 +33,9 @@ public interface Task extends ExecutionHandler { FailureHandler getFailureHandler(); DeadExecutionHandler getDeadExecutionHandler(); + + @Override + default String getTaskName() { + return getName(); + } } diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskDescriptor.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskDescriptor.java new file mode 100644 index 00000000..b97154aa --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskDescriptor.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.task; + +public interface TaskDescriptor extends HasTaskName { + + String getTaskName(); + Class getDataClass(); + + static TaskDescriptor of(String name, Class dataClass) { + return new TaskDescriptor.SimpleTaskDescriptor(name, dataClass); + } + + class SimpleTaskDescriptor implements TaskDescriptor { + + private final String name; + private final Class dataClass; + + public SimpleTaskDescriptor(String name, Class dataClass) { + this.name = name; + this.dataClass = dataClass; + } + + @Override + public String getTaskName() { + return name; + } + + @Override + public Class getDataClass() { + return dataClass; + } + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskInstanceId.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskInstanceId.java index 1bb47d5c..7ea39315 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskInstanceId.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskInstanceId.java @@ -17,7 +17,8 @@ import java.util.Objects; -public interface TaskInstanceId { +public interface TaskInstanceId extends HasTaskName { + @Override String getTaskName(); String getId(); static TaskInstanceId of(String taskName, String id) { diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithDataDescriptor.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithDataDescriptor.java new file mode 100644 index 00000000..38b8662d --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithDataDescriptor.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.task; + +/** + * Experimental + */ +public class TaskWithDataDescriptor implements TaskDescriptor { + + private final String taskName; + private final Class dataClass; + + public TaskWithDataDescriptor(String taskName, Class dataClass) { //TODO: not used? + this.taskName = taskName; + this.dataClass = dataClass; + } + + public TaskInstance instance(String id, T data) { + return new TaskInstance<>(taskName, id, data); + } + + @Override + public String getTaskName() { + return taskName; + } + + @Override + public Class getDataClass() { + return dataClass; + } + + public TaskInstanceId instanceId(String id) { + return TaskInstanceId.of(taskName, id); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithoutDataDescriptor.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithoutDataDescriptor.java new file mode 100644 index 00000000..9f0cb0b8 --- /dev/null +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/TaskWithoutDataDescriptor.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.scheduler.task; + +/** + * Experimental + */ +public class TaskWithoutDataDescriptor implements TaskDescriptor { + + private final String taskName; + + public TaskWithoutDataDescriptor(String taskName) { + this.taskName = taskName; + } + + public TaskInstance instance(String id) { + return new TaskInstance<>(taskName, id); + } + + @Override + public String getTaskName() { + return taskName; + } + + @Override + public Class getDataClass() { + return Void.class; + } + + public TaskInstanceId instanceId(String id) { + return TaskInstanceId.of(taskName, id); + } +} diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/PlainScheduleAndData.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/PlainScheduleAndData.java index 4c9b729b..07875de2 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/PlainScheduleAndData.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/PlainScheduleAndData.java @@ -24,6 +24,11 @@ public class PlainScheduleAndData implements ScheduleAndData, Serializable { private final Schedule schedule; private final Object data; + private PlainScheduleAndData() { // For serializers + schedule = null; + data = null; + } + public PlainScheduleAndData(Schedule schedule) { this.schedule = schedule; this.data = null; @@ -60,7 +65,7 @@ public int hashCode() { @Override public String toString() { - return this.getClass().getName() + "{" + + return this.getClass().getSimpleName() + "{" + "schedule=" + schedule + ", data=" + data + '}'; diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/ScheduleAndData.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/ScheduleAndData.java index d32e56bf..8606461b 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/ScheduleAndData.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/ScheduleAndData.java @@ -24,7 +24,7 @@ public interface ScheduleAndData extends Serializable { Schedule getSchedule(); Object getData(); - default PlainScheduleAndData of(Schedule schedule, Object data) { + static PlainScheduleAndData of(Schedule schedule, Object data) { return new PlainScheduleAndData(schedule, data); } } diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/Tasks.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/Tasks.java index bb61f4af..a0676ae8 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/Tasks.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/helper/Tasks.java @@ -15,9 +15,13 @@ */ package com.github.kagkarlsson.scheduler.task.helper; +import com.github.kagkarlsson.scheduler.Clock; +import com.github.kagkarlsson.scheduler.SchedulerClient; import com.github.kagkarlsson.scheduler.task.*; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; import com.github.kagkarlsson.scheduler.task.schedule.Schedule; +import java.io.ObjectStreamClass; import java.time.Duration; import java.time.Instant; import java.util.function.Function; @@ -26,34 +30,50 @@ public class Tasks { public static final Duration DEFAULT_RETRY_INTERVAL = Duration.ofMinutes(5); public static RecurringTaskBuilder recurring(String name, Schedule schedule) { - return new RecurringTaskBuilder<>(name, schedule, Void.class); + return recurring(TaskDescriptor.of(name, Void.class), schedule); } public static RecurringTaskBuilder recurring(String name, Schedule schedule, Class dataClass) { - return new RecurringTaskBuilder<>(name, schedule, dataClass); + return recurring(TaskDescriptor.of(name, dataClass), schedule); + } + + public static RecurringTaskBuilder recurring(TaskDescriptor descriptor, Schedule schedule) { + return new RecurringTaskBuilder<>(descriptor.getTaskName(), schedule, descriptor.getDataClass()); } public static RecurringTaskWithPersistentScheduleBuilder recurringWithPersistentSchedule(String name, Class dataClass) { - return new RecurringTaskWithPersistentScheduleBuilder(name, dataClass); + return recurringWithPersistentSchedule(TaskDescriptor.of(name, dataClass)); + } + + public static RecurringTaskWithPersistentScheduleBuilder recurringWithPersistentSchedule(TaskDescriptor descriptor) { + return new RecurringTaskWithPersistentScheduleBuilder<>(descriptor.getTaskName(), descriptor.getDataClass()); } public static OneTimeTaskBuilder oneTime(String name) { - return new OneTimeTaskBuilder<>(name, Void.class); + return oneTime(TaskDescriptor.of(name, Void.class)); } public static OneTimeTaskBuilder oneTime(String name, Class dataClass) { - return new OneTimeTaskBuilder<>(name, dataClass); + return oneTime(TaskDescriptor.of(name, dataClass)); + } + + public static OneTimeTaskBuilder oneTime(TaskDescriptor descriptor) { + return new OneTimeTaskBuilder<>(descriptor.getTaskName(), descriptor.getDataClass()); } public static TaskBuilder custom(String name, Class dataClass) { return new TaskBuilder<>(name, dataClass); } + public static TaskBuilder custom(TaskDescriptor taskDescriptor) { + return new TaskBuilder<>(taskDescriptor.getTaskName(), taskDescriptor.getDataClass()); + } + public static class RecurringTaskBuilder { private final String name; private final Schedule schedule; - private Class dataClass; + private final Class dataClass; private FailureHandler onFailure; private DeadExecutionHandler onDeadExecution; private ScheduleRecurringOnStartup scheduleOnStartup; @@ -92,6 +112,20 @@ public RecurringTaskBuilder initialData(T initialData) { return this; } + /** + * Disable 'scheduleOnStartup' to get control over when and show the executions is scheduled. + * Schedules will not be updated etc, so not really recommended. + */ + public RecurringTaskBuilder doNotScheduleOnStartup() { + this.scheduleOnStartup = new ScheduleRecurringOnStartup(RecurringTask.INSTANCE, null, null) { + @Override + public void apply(SchedulerClient scheduler, Clock clock, Task task) { + // do nothing + } + }; + return this; + } + public RecurringTask execute(VoidExecutionHandler executionHandler) { return new RecurringTask(name, schedule, dataClass, scheduleOnStartup, onFailure, onDeadExecution) { @@ -173,7 +207,7 @@ public CompletionHandler execute(TaskInstance taskInstance, ExecutionConte public static class OneTimeTaskBuilder { private final String name; - private Class dataClass; + private final Class dataClass; private FailureHandler onFailure; private DeadExecutionHandler onDeadExecution; @@ -216,7 +250,7 @@ public void executeOnce(TaskInstance taskInstance, ExecutionContext execution public static class TaskBuilder { private final String name; - private Class dataClass; + private final Class dataClass; private FailureHandler onFailure; private DeadExecutionHandler onDeadExecution; private ScheduleOnStartup onStartup; diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/CronSchedule.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/CronSchedule.java index b722d0a6..ace79218 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/CronSchedule.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/CronSchedule.java @@ -43,6 +43,10 @@ public class CronSchedule implements Schedule, Serializable { private final ZoneId zoneId; private transient ExecutionTime cronExecutionTime; // lazily initialized + private CronSchedule() { // For serializers + pattern = null; + zoneId = ZoneId.systemDefault(); + } public CronSchedule(String pattern) { this(pattern, ZoneId.systemDefault()); } diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/Daily.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/Daily.java index eb3f1da8..8d178b79 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/Daily.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/Daily.java @@ -33,6 +33,11 @@ public class Daily implements Schedule, Serializable { private final List times; private final ZoneId zone; + private Daily() { // For serializers + zone = ZoneId.systemDefault(); + times = null; + } + public Daily(LocalTime... times) { this(ZoneId.systemDefault(), Arrays.asList(times)); } diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/FixedDelay.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/FixedDelay.java index e4c97f1e..21b838ba 100644 --- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/FixedDelay.java +++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/task/schedule/FixedDelay.java @@ -26,6 +26,9 @@ public class FixedDelay implements Schedule, Serializable { private final Duration duration; + private FixedDelay() { // For serializers + duration = null; + } private FixedDelay(Duration duration) { this.duration = Objects.requireNonNull(duration); } diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/gson/GsonSerializerTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/gson/GsonSerializerTest.java index 029bfaeb..5ca7a836 100644 --- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/gson/GsonSerializerTest.java +++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/gson/GsonSerializerTest.java @@ -1,23 +1,85 @@ package com.github.kagkarlsson.scheduler.serializer.gson; import com.github.kagkarlsson.scheduler.serializer.GsonSerializer; +import com.github.kagkarlsson.scheduler.serializer.jackson.ScheduleAndDataForTest; +import com.github.kagkarlsson.scheduler.task.ExecutionComplete; +import com.github.kagkarlsson.scheduler.task.helper.PlainScheduleAndData; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; +import com.github.kagkarlsson.scheduler.task.schedule.Daily; +import com.github.kagkarlsson.scheduler.task.schedule.FixedDelay; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; import com.google.gson.Gson; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; +import java.time.LocalTime; import static org.junit.jupiter.api.Assertions.assertEquals; public class GsonSerializerTest { + private GsonSerializer serializer; + + @BeforeEach + public void setUp() { + serializer = new GsonSerializer(); + } + @Test public void serialize_instant() { - final GsonSerializer gsonSerializer = new GsonSerializer(); final Instant now = Instant.now(); - assertEquals(now, gsonSerializer.deserialize(Instant.class, gsonSerializer.serialize(now))); + assertEquals(now, serializer.deserialize(Instant.class, serializer.serialize(now))); + } + + @Test + public void serialize_cron() { + CronSchedule cronSchedule = new CronSchedule("* * * * * *"); + assertEquals(cronSchedule.getPattern(), serializer.deserialize(CronSchedule.class, serializer.serialize(cronSchedule)).getPattern()); + } + + @Test + public void serialize_daily() { + Instant now = Instant.now(); + ExecutionComplete executionComplete = ExecutionComplete.simulatedSuccess(now); + + Daily daily = Schedules.daily(LocalTime.MIDNIGHT); + Daily deserialized = serializer.deserialize(Daily.class, serializer.serialize(daily)); + assertEquals(daily.getNextExecutionTime(executionComplete), deserialized.getNextExecutionTime(executionComplete)); } + @Test + public void serialize_fixed_delay() { + Instant now = Instant.now(); + ExecutionComplete executionComplete = ExecutionComplete.simulatedSuccess(now); + + FixedDelay fixedDelay = Schedules.fixedDelay(Duration.ofHours(1)); + FixedDelay deserialized = serializer.deserialize(FixedDelay.class, serializer.serialize(fixedDelay)); + + assertEquals(fixedDelay.getNextExecutionTime(executionComplete), deserialized.getNextExecutionTime(executionComplete)); + } + + @Test + public void serialize_schedule_and_data() { + Instant now = Instant.now(); + CronSchedule cronSchedule = new CronSchedule("* * * * * *"); + + ScheduleAndDataForTest scheduleAndData = new ScheduleAndDataForTest(cronSchedule, 50L); + ScheduleAndDataForTest deserialized = serializer.deserialize(ScheduleAndDataForTest.class, serializer.serialize(scheduleAndData)); + + assertEquals(scheduleAndData.getSchedule().getInitialExecutionTime(now), deserialized.getSchedule().getInitialExecutionTime(now)); + assertEquals(50, deserialized.getData()); + } + + private static class CustomData { + private int id; + + public CustomData(int id) { + + this.id = id; + } + } } diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/JacksonSerializerTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/JacksonSerializerTest.java index 4078d52c..75440191 100644 --- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/JacksonSerializerTest.java +++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/JacksonSerializerTest.java @@ -1,21 +1,86 @@ package com.github.kagkarlsson.scheduler.serializer.jackson; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.github.kagkarlsson.scheduler.serializer.JacksonSerializer; +import com.github.kagkarlsson.scheduler.task.helper.PlainScheduleAndData; +import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; +import com.github.kagkarlsson.scheduler.task.schedule.Daily; +import com.github.kagkarlsson.scheduler.task.schedule.FixedDelay; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.Serializable; +import java.time.Duration; import java.time.Instant; +import java.time.LocalTime; import static org.junit.jupiter.api.Assertions.assertEquals; public class JacksonSerializerTest { + private JacksonSerializer serializer; + + @BeforeEach + public void setUp() { + serializer = new JacksonSerializer(); + } + @Test public void serialize_instant() { - JacksonSerializer serializer = new JacksonSerializer(); - final Instant now = Instant.now(); assertEquals(now, serializer.deserialize(Instant.class, serializer.serialize(now))); } + @Test + public void serialize_cron() { + CronSchedule cronSchedule = new CronSchedule("* * * * * *"); + assertEquals(cronSchedule.getPattern(), serializer.deserialize(CronSchedule.class, serializer.serialize(cronSchedule)).getPattern()); + } + + @Test + public void serialize_daily() { + Instant now = Instant.now(); + Daily daily = Schedules.daily(LocalTime.MIDNIGHT); + Daily deserialized = serializer.deserialize(Daily.class, serializer.serialize(daily)); + assertEquals(daily.getInitialExecutionTime(now), deserialized.getInitialExecutionTime(now)); + } + + @Test + public void serialize_fixed_delay() { + Instant now = Instant.now(); + FixedDelay fixedDelay = Schedules.fixedDelay(Duration.ofHours(1)); + FixedDelay deserialized = serializer.deserialize(FixedDelay.class, serializer.serialize(fixedDelay)); + assertEquals(fixedDelay.getInitialExecutionTime(now), deserialized.getInitialExecutionTime(now)); + } + + @Test + public void serialize_plain_schedule_and_data() { + // Not recommended to use abstract types in data that is json-serialized, type-information is destroyed + Instant now = Instant.now(); + FixedDelay fixedDelay = Schedules.fixedDelay(Duration.ofHours(1)); + + PlainScheduleAndData plainScheduleAndData = new PlainScheduleAndData(fixedDelay, 50); + PlainScheduleAndData deserialized = serializer.deserialize(PlainScheduleAndData.class, serializer.serialize(plainScheduleAndData)); + + assertEquals(plainScheduleAndData.getSchedule().getInitialExecutionTime(now), deserialized.getSchedule().getInitialExecutionTime(now)); + assertEquals(50, deserialized.getData()); + } + + @Test + public void serialize_schedule_and_data() { + Instant now = Instant.now(); + CronSchedule cronSchedule = new CronSchedule("* * * * * *"); + + ScheduleAndDataForTest scheduleAndData = new ScheduleAndDataForTest(cronSchedule, 50L); + ScheduleAndDataForTest deserialized = serializer.deserialize(ScheduleAndDataForTest.class, serializer.serialize(scheduleAndData)); + + assertEquals(scheduleAndData.getSchedule().getInitialExecutionTime(now), deserialized.getSchedule().getInitialExecutionTime(now)); + assertEquals(50, deserialized.getData()); + } + } diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleAndDataForTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleAndDataForTest.java new file mode 100644 index 00000000..22d49321 --- /dev/null +++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/serializer/jackson/ScheduleAndDataForTest.java @@ -0,0 +1,29 @@ +package com.github.kagkarlsson.scheduler.serializer.jackson; + +import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; + +public class ScheduleAndDataForTest implements ScheduleAndData { + private static final long serialVersionUID = 1L; // recommended when using Java serialization + private final CronSchedule schedule; + private final Long id; + + private ScheduleAndDataForTest() { + this(null, null); + } + + public ScheduleAndDataForTest(CronSchedule schedule, Long id) { + this.schedule = schedule; + this.id = id; + } + + @Override + public CronSchedule getSchedule() { + return schedule; + } + + @Override + public Long getData() { + return id; + } +} diff --git a/examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingSeparateTasksMain.java b/examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingSeparateTasksMain.java index dc9fcc8c..c7f8fee1 100644 --- a/examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingSeparateTasksMain.java +++ b/examples/features/src/main/java/com/github/kagkarlsson/examples/JobChainingUsingSeparateTasksMain.java @@ -1,12 +1,12 @@ /** * Copyright (C) Gustav Karlsson - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

+ * * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/examples/features/src/main/java/com/github/kagkarlsson/examples/RecurringTaskWithPersistentScheduleMain.java b/examples/features/src/main/java/com/github/kagkarlsson/examples/RecurringTaskWithPersistentScheduleMain.java index 7b51d408..621f98d5 100644 --- a/examples/features/src/main/java/com/github/kagkarlsson/examples/RecurringTaskWithPersistentScheduleMain.java +++ b/examples/features/src/main/java/com/github/kagkarlsson/examples/RecurringTaskWithPersistentScheduleMain.java @@ -17,18 +17,13 @@ import com.github.kagkarlsson.examples.helpers.Example; import com.github.kagkarlsson.scheduler.Scheduler; -import com.github.kagkarlsson.scheduler.task.helper.PlainScheduleAndData; import com.github.kagkarlsson.scheduler.task.helper.RecurringTaskWithPersistentSchedule; import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; import com.github.kagkarlsson.scheduler.task.helper.Tasks; -import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; -import com.github.kagkarlsson.scheduler.task.schedule.PersistentCronSchedule; -import com.github.kagkarlsson.scheduler.task.schedule.Schedule; -import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import com.github.kagkarlsson.scheduler.task.schedule.*; import javax.sql.DataSource; import java.time.Duration; -import java.time.ZoneId; public class RecurringTaskWithPersistentScheduleMain extends Example { @@ -39,8 +34,8 @@ public static void main(String[] args) { @Override public void run(DataSource dataSource) { - final RecurringTaskWithPersistentSchedule task = - Tasks.recurringWithPersistentSchedule("dynamic-recurring-task", PlainScheduleAndData.class) + final RecurringTaskWithPersistentSchedule task = + Tasks.recurringWithPersistentSchedule("dynamic-recurring-task", ScheduleAndNoData.class) .execute((taskInstance, executionContext) -> { System.out.println("Instance: '" + taskInstance.getId() + "' ran using persistent schedule: " + taskInstance.getData().getSchedule()); }); @@ -54,8 +49,31 @@ public void run(DataSource dataSource) { scheduler.start(); sleep(2_000); - scheduler.schedule(task.schedulableInstance("id1", new PlainScheduleAndData(Schedules.fixedDelay(Duration.ofSeconds(1))))); - scheduler.schedule(task.schedulableInstance("id2", new PlainScheduleAndData(Schedules.fixedDelay(Duration.ofSeconds(6))))); + scheduler.schedule(task.schedulableInstance("id1", new ScheduleAndNoData(Schedules.fixedDelay(Duration.ofSeconds(1))))); + scheduler.schedule(task.schedulableInstance("id2", new ScheduleAndNoData(Schedules.fixedDelay(Duration.ofSeconds(6))))); + } + + public static class ScheduleAndNoData implements ScheduleAndData { + private static final long serialVersionUID = 1L; // recommended when using Java serialization + private final FixedDelay schedule; + + private ScheduleAndNoData() { + this(null); + } + + public ScheduleAndNoData(FixedDelay schedule) { + this.schedule = schedule; + } + + @Override + public FixedDelay getSchedule() { + return schedule; + } + + @Override + public Object getData() { + return null; + } } } diff --git a/examples/features/src/main/java/com/github/kagkarlsson/examples/StatefulRecurringTaskWithPersistentScheduleMain.java b/examples/features/src/main/java/com/github/kagkarlsson/examples/StatefulRecurringTaskWithPersistentScheduleMain.java index 207d95a1..d37b6965 100644 --- a/examples/features/src/main/java/com/github/kagkarlsson/examples/StatefulRecurringTaskWithPersistentScheduleMain.java +++ b/examples/features/src/main/java/com/github/kagkarlsson/examples/StatefulRecurringTaskWithPersistentScheduleMain.java @@ -19,6 +19,7 @@ import com.github.kagkarlsson.scheduler.Scheduler; import com.github.kagkarlsson.scheduler.task.helper.PlainScheduleAndData; import com.github.kagkarlsson.scheduler.task.helper.RecurringTaskWithPersistentSchedule; +import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; import com.github.kagkarlsson.scheduler.task.helper.Tasks; import com.github.kagkarlsson.scheduler.task.schedule.Schedule; import com.github.kagkarlsson.scheduler.task.schedule.Schedules; @@ -57,13 +58,31 @@ public void run(DataSource dataSource) { 1))); } - public static class ScheduleAndInteger extends PlainScheduleAndData { + public static class ScheduleAndInteger implements ScheduleAndData { + private final Schedule schedule; + private final Integer data; + + public ScheduleAndInteger() { + this(null, null); + } + public ScheduleAndInteger(Schedule schedule, Integer data) { - super(schedule, data); + this.schedule = schedule; + this.data = data; + } + + @Override + public Schedule getSchedule() { + return schedule; + } + + @Override + public Integer getData() { + return data; } public ScheduleAndInteger returnIncremented() { - return new ScheduleAndInteger(super.getSchedule(), ((Integer) super.getData()) + 1); + return new ScheduleAndInteger(getSchedule(), getData() + 1); } } diff --git a/examples/spring-boot-example/README.md b/examples/spring-boot-example/README.md index dd8403dd..42763a76 100644 --- a/examples/spring-boot-example/README.md +++ b/examples/spring-boot-example/README.md @@ -1,3 +1,26 @@ # Spring Boot Example This Maven module provides a working example of the [db-scheduler](https://github.com/kagkarlsson/db-scheduler) running in a Spring Boot application using the provided Spring Boot starter. + + +**Task-names for the examples** + +* sample-one-time-task +* chained-step-1 +* long-running-task +* multi-instance-recurring-task +* parallel-job-spawner +* state-tracking-recurring-task +* transactionally-staged-task + + +## Running examples + +Examples need to be started, currently by sending a request to the web-server: + +```shell +curl -X POST http://localhost:8080/admin/start -H "Content-Type: application/json" -d '{"taskName":"sample-one-time-task"}' +``` + +Replace `taskName` with one of the examples from above. + diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/AdminController.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/AdminController.java new file mode 100644 index 00000000..23880b88 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/AdminController.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot; + +import com.github.kagkarlsson.examples.boot.config.*; +import com.github.kagkarlsson.scheduler.SchedulerClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/admin") +public class AdminController { + private static final Logger LOG = LoggerFactory.getLogger(AdminController.class); + + // Endpoints + public static final String LIST_SCHEDULED = "/tasks"; + public static final String START = "/start"; + public static final String STOP = "/stop"; + + private static final Map> TASK_STARTERS = new HashMap<>(); + static { + TASK_STARTERS.put(BasicExamplesConfiguration.BASIC_ONE_TIME_TASK.getTaskName(), BasicExamplesConfiguration::triggerOneTime); + TASK_STARTERS.put(TransactionallyStagedJobConfiguration.TRANSACTIONALLY_STAGED_TASK.getTaskName(), TransactionallyStagedJobConfiguration::start); + TASK_STARTERS.put(JobChainingConfiguration.CHAINED_STEP_1_TASK.getTaskName(), JobChainingConfiguration::start); + TASK_STARTERS.put(ParallellJobConfiguration.PARALLEL_JOB_SPAWNER.getTaskName(), ParallellJobConfiguration::start); + TASK_STARTERS.put(LongRunningJobConfiguration.LONG_RUNNING_TASK.getTaskName(), LongRunningJobConfiguration::start); + TASK_STARTERS.put(MultiInstanceRecurringConfiguration.MULTI_INSTANCE_RECURRING_TASK.getTaskName(), MultiInstanceRecurringConfiguration::start); + TASK_STARTERS.put(RecurringStateTrackingConfiguration.STATE_TRACKING_RECURRING_TASK.getTaskName(), RecurringStateTrackingConfiguration::start); + } + + private final SchedulerClient schedulerClient; + private final ExampleContext exampleContext; + + public AdminController(SchedulerClient schedulerClient, TransactionTemplate tx) { + this.schedulerClient = schedulerClient; + exampleContext = new ExampleContext(schedulerClient, tx, LOG); + } + + @GetMapping(path = LIST_SCHEDULED) + public List list() { + return schedulerClient.getScheduledExecutions().stream() + .map(e -> { + return new Scheduled( + e.getTaskInstance().getTaskName(), + e.getTaskInstance().getId(), + e.getExecutionTime(), + e.getData()); + }) + .collect(Collectors.toList()); + } + + @PostMapping(path = START, headers = {"Content-type=application/json"}) + public void start(@RequestBody StartRequest request) { + TASK_STARTERS.get(request.taskName).accept(exampleContext); + } + + @PostMapping(path = STOP, headers = {"Content-type=application/json"}) + public void stop(@RequestBody StartRequest request) { + schedulerClient.getScheduledExecutions().stream() + .filter(s -> s.getTaskInstance().getTaskName().equals(request.taskName)) + .findAny() + .ifPresent(s -> schedulerClient.cancel(s.getTaskInstance())); + } + + public static class StartRequest { + public final String taskName; + + public StartRequest() { + this(""); + } + + public StartRequest(String taskName) { + this.taskName = taskName; + } + } + + public static class Scheduled { + public final String taskName; + public final String id; + public final Instant executionTime; + public final Object data; + + public Scheduled() { + this(null, null, null, null); + } + + public Scheduled(String taskName, String id, Instant executionTime, Object data) { + this.taskName = taskName; + this.id = id; + this.executionTime = executionTime; + this.data = data; + } + } +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/App.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/App.java index 78d5d50a..00351791 100644 --- a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/App.java +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/App.java @@ -18,19 +18,25 @@ import com.github.kagkarlsson.scheduler.Scheduler; import com.github.kagkarlsson.scheduler.task.Task; import java.time.Instant; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @SpringBootApplication public class App { private static final Logger log = LoggerFactory.getLogger(App.class); public static void main(String... args) { - SpringApplication.run(App.class, args); + + ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args); + } /** diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/ExampleContext.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/ExampleContext.java new file mode 100644 index 00000000..c108c158 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/ExampleContext.java @@ -0,0 +1,21 @@ +package com.github.kagkarlsson.examples.boot; + +import com.github.kagkarlsson.scheduler.SchedulerClient; +import org.slf4j.Logger; +import org.springframework.transaction.support.TransactionTemplate; + +public class ExampleContext { + public SchedulerClient schedulerClient; + public TransactionTemplate tx; + private Logger logger; + + public ExampleContext(SchedulerClient schedulerClient, TransactionTemplate tx, Logger logger) { + this.schedulerClient = schedulerClient; + this.tx = tx; + this.logger = logger; + } + + public void log(String message) { + logger.info(message); + } +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TaskConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/BasicExamplesConfiguration.java similarity index 57% rename from examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TaskConfiguration.java rename to examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/BasicExamplesConfiguration.java index f5dc9349..283f219e 100644 --- a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TaskConfiguration.java +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/BasicExamplesConfiguration.java @@ -18,21 +18,39 @@ import static com.github.kagkarlsson.scheduler.task.schedule.Schedules.fixedDelay; import com.github.kagkarlsson.examples.boot.CounterService; -import com.github.kagkarlsson.scheduler.SchedulerName; -import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer; +import com.github.kagkarlsson.examples.boot.ExampleContext; import com.github.kagkarlsson.scheduler.task.Task; +import com.github.kagkarlsson.scheduler.task.TaskWithoutDataDescriptor; import com.github.kagkarlsson.scheduler.task.helper.Tasks; import java.time.Duration; -import java.util.Optional; +import java.time.Instant; +import utils.EventLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -public class TaskConfiguration { - private static final Logger log = LoggerFactory.getLogger(TaskConfiguration.class); +public class BasicExamplesConfiguration { + + public static final TaskWithoutDataDescriptor BASIC_ONE_TIME_TASK = new TaskWithoutDataDescriptor("sample-one-time-task"); + public static final TaskWithoutDataDescriptor BASIC_RECURRING_TASK = new TaskWithoutDataDescriptor("recurring-sample-task"); + private static final Logger log = LoggerFactory.getLogger(BasicExamplesConfiguration.class); + private static int ID = 1; + + + /** Start the example */ + public static void triggerOneTime(ExampleContext ctx) { + ctx.log("Scheduling a basic one-time task to run 'Instant.now()+seconds'. If seconds=0, the scheduler will pick " + + "these up immediately since it is configured with 'immediate-execution-enabled=true'" + ); + + ctx.schedulerClient.schedule( + BASIC_ONE_TIME_TASK.instance(String.valueOf(ID++)), + Instant.now() + ); + } /** * Define a recurring task with a dependency, which will automatically be picked up by the @@ -41,10 +59,11 @@ public class TaskConfiguration { @Bean Task recurringSampleTask(CounterService counter) { return Tasks - .recurring("recurring-sample-task", fixedDelay(Duration.ofMinutes(1))) + .recurring(BASIC_RECURRING_TASK, fixedDelay(Duration.ofMinutes(1))) .execute((instance, ctx) -> { log.info("Running recurring-simple-task. Instance: {}, ctx: {}", instance, ctx); counter.increase(); + EventLogger.logTask(BASIC_RECURRING_TASK, "Ran. Run-counter current-value="+counter.read()); }); } @@ -53,22 +72,10 @@ Task recurringSampleTask(CounterService counter) { */ @Bean Task sampleOneTimeTask() { - return Tasks.oneTime("sample-one-time-task") + return Tasks.oneTime(BASIC_ONE_TIME_TASK) .execute((instance, ctx) -> { log.info("I am a one-time task!"); }); } - /** - * Bean defined when a configuration-property in DbSchedulerCustomizer needs to be overridden. - */ - @Bean - DbSchedulerCustomizer customizer() { - return new DbSchedulerCustomizer() { - @Override - public Optional schedulerName() { - return Optional.of(new SchedulerName.Fixed("spring-boot-scheduler-1")); - } - }; - } } diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/JobChainingConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/JobChainingConfiguration.java new file mode 100644 index 00000000..706f33c9 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/JobChainingConfiguration.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.CompletionHandler; +import com.github.kagkarlsson.scheduler.task.TaskWithDataDescriptor; +import com.github.kagkarlsson.scheduler.task.helper.CustomTask; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import utils.EventLogger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; + +@Configuration +public class JobChainingConfiguration { + + public static final TaskWithDataDescriptor CHAINED_STEP_1_TASK = new TaskWithDataDescriptor<>("chained-step-1", JobState.class); + public static final TaskWithDataDescriptor CHAINED_STEP_2_TASK = new TaskWithDataDescriptor<>("chained-step-2", JobState.class); + public static final TaskWithDataDescriptor CHAINED_STEP_3_TASK = new TaskWithDataDescriptor<>("chained-step-3", JobState.class); + private static int CHAINED_JOB_ID = 1; + + + /** Start the example */ + public static void start(ExampleContext ctx) { + ctx.log("Scheduling a chained one-time task to run."); + + int id = CHAINED_JOB_ID++; + ctx.schedulerClient.schedule( + CHAINED_STEP_1_TASK.instance("chain-" + id, new JobState(id, 0)), + Instant.now() + ); + } + + /** Bean definition */ + @Bean + public CustomTask chainedStep1() { + return Tasks.custom(CHAINED_STEP_1_TASK) + .execute((taskInstance, executionContext) -> { + JobState data = taskInstance.getData(); + EventLogger.logTask(CHAINED_STEP_1_TASK, "Ran step1. Schedules step2 after successful run. Data: " + data); + + JobState nextJobState = new JobState(data.id, data.counter + 1); + EventLogger.logTask(CHAINED_STEP_1_TASK, "Ran step1. Schedules step2 after successful run. Data: " + nextJobState); + return new CompletionHandler.OnCompleteReplace<>(CHAINED_STEP_2_TASK, nextJobState); + }); + } + + /** Bean definition */ + @Bean + public CustomTask chainedStep2() { + return Tasks.custom(CHAINED_STEP_2_TASK) + .execute((taskInstance, executionContext) -> { + JobState data = taskInstance.getData(); + JobState nextJobState = new JobState(data.id, data.counter + 1); + + // simulate we are waiting for some condition to be fulfilled before continuing to next step + if (nextJobState.counter >= 5) { + EventLogger.logTask(CHAINED_STEP_2_TASK, "Ran step2. Condition met. Schedules final step (step3) after successful run. Data: " + data); + return new CompletionHandler.OnCompleteReplace<>(CHAINED_STEP_3_TASK, nextJobState); + } else { + EventLogger.logTask(CHAINED_STEP_2_TASK, "Ran step2. Condition for progressing not yet met, rescheduling. Data: " + data); + return new CompletionHandler.OnCompleteReschedule<>(Schedules.fixedDelay(Duration.ofSeconds(5)), nextJobState); + } + }); + } + + /** Bean definition */ + @Bean + public CustomTask chainedStep3() { + return Tasks.custom(CHAINED_STEP_3_TASK) + .execute((taskInstance, executionContext) -> { + EventLogger.logTask(CHAINED_STEP_3_TASK, "Ran step3. This was the final step in the processing, removing. Data: " + taskInstance.getData()); + return new CompletionHandler.OnCompleteRemove<>(); // same as for one-time tasks + }); + } + public static class JobState implements Serializable { + + private static final long serialVersionUID = 1L; // recommended when using Java serialization + public final int id; + public final int counter; + + public JobState() { + this(0, 0); + } // for serializing + + public JobState(int id, int counter) { + this.id = id; + this.counter = counter; + } + + @Override + public String toString() { + return "JobState{" + + "id=" + id + + ", counter=" + counter + + '}'; + } + } + +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/LongRunningJobConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/LongRunningJobConfiguration.java new file mode 100644 index 00000000..5744ab34 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/LongRunningJobConfiguration.java @@ -0,0 +1,125 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.CompletionHandler; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import com.github.kagkarlsson.scheduler.task.TaskWithDataDescriptor; +import com.github.kagkarlsson.scheduler.task.helper.CustomTask; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import utils.EventLogger; + +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; + +@Configuration +public class LongRunningJobConfiguration { + + public static final TaskWithDataDescriptor LONG_RUNNING_TASK = new TaskWithDataDescriptor<>("long-running-task", PrimeGeneratorState.class); + + + /** Start the example */ + public static void start(ExampleContext ctx) { + ctx.log("Scheduling long-running task "+ LONG_RUNNING_TASK.getTaskName()+" to run 3s at a time until it " + + "has found all prime-numbers smaller than 1.000.000."); + + PrimeGeneratorState initialState = new PrimeGeneratorState(0, 0); + ctx.schedulerClient.schedule( + LONG_RUNNING_TASK.instance("prime-generator", initialState), + Instant.now() + ); + } + + /** Bean definition */ + @Bean + public CustomTask longRunningTask() { + return Tasks.custom(LONG_RUNNING_TASK) + .execute((TaskInstance taskInstance, ExecutionContext executionContext) -> { + EventLogger.logTask(LONG_RUNNING_TASK, "Continuing prime-number generation from: " + taskInstance.getData()); + + long currentNumber = taskInstance.getData().lastTestedNumber; + long lastFoundPrime = taskInstance.getData().lastFoundPrime; + long start = System.currentTimeMillis(); + + // For long-running tasks, it is important to regularly check that + // conditions allow us to continue executing + // Here, we pretend that the execution may only run 3s straight, a more realistic + // scenario is for example executions that may only run nightly, in the off-hours + while (!executionContext.getSchedulerState().isShuttingDown() + && maxSecondsSince(start, 3)) { + + if (isPrime(currentNumber)) { + lastFoundPrime = currentNumber; + } + currentNumber++; + } + + // Make the decision on whether to reschedule at a later time or terminate/remove + if (currentNumber > 1_000_000) { + // lets say 1M is the end-condition for our long-running task + return new CompletionHandler.OnCompleteRemove<>(); + } else { + // save state and reschedule when conditions do not allow the execution to run anymore + PrimeGeneratorState stateToSave = new PrimeGeneratorState(lastFoundPrime, currentNumber); + EventLogger.logTask(LONG_RUNNING_TASK, "Ran for 3s. Saving state for next run. Current state: " + stateToSave); + return new CompletionHandler.OnCompleteReschedule<>(Schedules.fixedDelay(Duration.ofSeconds(10)), stateToSave); + } + }); + } + + private boolean maxSecondsSince(long start, long seconds) { + return System.currentTimeMillis() - start < seconds * 1000; + } + + private boolean isPrime(long currentNumber) { + for (long i = 2; i < currentNumber; i++) { + if (currentNumber % i == 0) { + // divisible + return false; + } + } + return true; + } + + public static class PrimeGeneratorState implements Serializable { + private static final long serialVersionUID = 1L; // recommended when using Java serialization + public final long lastFoundPrime; + public final long lastTestedNumber; + + public PrimeGeneratorState() { + this(0, 0); + } // for serializing + + public PrimeGeneratorState(long lastFoundPrime, long lastTestedNumber) { + this.lastFoundPrime = lastFoundPrime; + this.lastTestedNumber = lastTestedNumber; + } + + @Override + public String toString() { + return "PrimeGeneratorState{" + + "lastFoundPrime=" + lastFoundPrime + + ", lastTestedNumber=" + lastTestedNumber + + '}'; + } + } +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/MultiInstanceRecurringConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/MultiInstanceRecurringConfiguration.java new file mode 100644 index 00000000..49227a5c --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/MultiInstanceRecurringConfiguration.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.Task; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import com.github.kagkarlsson.scheduler.task.TaskWithDataDescriptor; +import com.github.kagkarlsson.scheduler.task.helper.ScheduleAndData; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import com.github.kagkarlsson.scheduler.task.schedule.CronSchedule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import utils.EventLogger; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Random; + +@Configuration +public class MultiInstanceRecurringConfiguration { + + public static final TaskWithDataDescriptor MULTI_INSTANCE_RECURRING_TASK = new TaskWithDataDescriptor<>("multi-instance-recurring-task", ScheduleAndCustomer.class); + + + /** Start the example */ + public static void start(ExampleContext ctx) { + CronSchedule cron = new CronSchedule(String.format("%s * * * * *", new Random().nextInt(59))); + Customer customer = new Customer(String.valueOf(new Random().nextInt(10000))); + ScheduleAndCustomer data = new ScheduleAndCustomer(cron, customer); + + ctx.log("Scheduling instance of recurring task "+ MULTI_INSTANCE_RECURRING_TASK.getTaskName()+" with data: " + data); + + ctx.schedulerClient.schedule( + MULTI_INSTANCE_RECURRING_TASK.instance(customer.id, data), + cron.getInitialExecutionTime(Instant.now()) + ); + } + + /** Bean definition */ + @Bean + public Task multiInstanceRecurring() { + // This task will only start running when at least one instance of the task has been scheduled + return Tasks.recurringWithPersistentSchedule(MULTI_INSTANCE_RECURRING_TASK) + .execute((TaskInstance taskInstance, ExecutionContext executionContext) -> { + + ScheduleAndCustomer data = taskInstance.getData(); + EventLogger.logTask(MULTI_INSTANCE_RECURRING_TASK, + String.format("Ran according to schedule '%s' for customer %s", data.getSchedule(), data.getData())); + }); + } + + public static class ScheduleAndCustomer implements ScheduleAndData { + private static final long serialVersionUID = 1L; // recommended when using Java serialization + private final CronSchedule schedule; + private final Customer customer; + + private ScheduleAndCustomer(){ this(null, null);} // + public ScheduleAndCustomer(CronSchedule schedule, Customer customer) { + this.schedule = schedule; + this.customer = customer; + } + + @Override + public CronSchedule getSchedule() { + return schedule; + } + + @Override + public Customer getData() { + return customer; + } + + @Override + public String toString() { + return "ScheduleAndCustomer{" + + "schedule=" + schedule + + ", customer=" + customer + + '}'; + } + } + + public static class Customer implements Serializable { + private static final long serialVersionUID = 1L; // recommended when using Java serialization + public final String id; + + private Customer() {this(null);} + public Customer(String id) { + this.id = id; + } + + @Override + public String toString() { + return "Customer{" + + "id='" + id + '\'' + + '}'; + } + } +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/ParallellJobConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/ParallellJobConfiguration.java new file mode 100644 index 00000000..942594cb --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/ParallellJobConfiguration.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.*; +import com.github.kagkarlsson.scheduler.task.helper.RecurringTask; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import utils.EventLogger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionTemplate; +import utils.Utils; + +import java.time.Instant; +import java.util.Random; + +@Configuration +public class ParallellJobConfiguration { + + public static final TaskWithoutDataDescriptor PARALLEL_JOB_SPAWNER = new TaskWithoutDataDescriptor("parallel-job-spawner"); + public static final TaskWithDataDescriptor PARALLEL_JOB = new TaskWithDataDescriptor<>("parallel-job", Integer.class); + private TransactionTemplate tx; + + public ParallellJobConfiguration(TransactionTemplate tx) { + this.tx = tx; + } + + + /** Start the example */ + public static void start(ExampleContext ctx) { + ctx.log("Starting recurring task "+ PARALLEL_JOB_SPAWNER.getTaskName()+". Initial execution-time will be now (deviating from defined schedule)."); + + ctx.schedulerClient.reschedule( + PARALLEL_JOB_SPAWNER.instanceId(RecurringTask.INSTANCE), + Instant.now() + ); + } + + /** Bean definition */ + @Bean + public Task parallelJobSpawner() { + return Tasks.recurring(PARALLEL_JOB_SPAWNER, Schedules.cron("0/20 * * * * *")) + .doNotScheduleOnStartup() // just for demo-purposes, so we can start it on-demand + .execute((TaskInstance taskInstance, ExecutionContext executionContext) -> { + + // Create all or none. SchedulerClient is transactions-aware since a Spring datasource is used + tx.executeWithoutResult((TransactionStatus status) -> { + for (int quarter = 1; quarter < 5; quarter++) { + // can use 'executionContext.getSchedulerClient()' to avoid circular dependency + executionContext.getSchedulerClient().schedule(PARALLEL_JOB.instance("q"+quarter, quarter), Instant.now()); + } + }); + EventLogger.logTask(PARALLEL_JOB_SPAWNER, "Ran. Scheduled tasks for generating quarterly report."); + }); + } + + @Bean + public Task parallelJob() { + return Tasks.oneTime(PARALLEL_JOB) + .execute((TaskInstance taskInstance, ExecutionContext executionContext) -> { + long startTime = System.currentTimeMillis(); + + Utils.sleep(new Random().nextInt(10) *1000); + + String threadName = Thread.currentThread().getName(); + EventLogger.logTask(PARALLEL_JOB, String.format("Ran. Generated report for quarter Q%s (in thread '%s', duration %sms)", taskInstance.getData(), threadName, System.currentTimeMillis() - startTime)); + }); + } + +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/RecurringStateTrackingConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/RecurringStateTrackingConfiguration.java new file mode 100644 index 00000000..47f9aed1 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/RecurringStateTrackingConfiguration.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.Task; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import com.github.kagkarlsson.scheduler.task.TaskWithDataDescriptor; +import com.github.kagkarlsson.scheduler.task.helper.RecurringTask; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import com.github.kagkarlsson.scheduler.task.schedule.Schedules; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import utils.EventLogger; + +import java.time.Instant; + +@Configuration +public class RecurringStateTrackingConfiguration { + + public static final TaskWithDataDescriptor STATE_TRACKING_RECURRING_TASK = new TaskWithDataDescriptor<>("state-tracking-recurring-task", Integer.class); + + + /** Start the example */ + public static void start(ExampleContext ctx) { + Integer data = 1; + ctx.log("Starting recurring task " + STATE_TRACKING_RECURRING_TASK.getTaskName() + + " with initial data: " + data + ". Initial execution-time will be now (deviating from defined schedule)."); + + ctx.schedulerClient.schedule( + STATE_TRACKING_RECURRING_TASK.instance(RecurringTask.INSTANCE, data), + Instant.now() // start-time, will run according to schedule after this + ); + } + + /** Bean definition */ + @Bean + public Task stateTrackingRecurring() { + return Tasks.recurring(STATE_TRACKING_RECURRING_TASK, Schedules.cron("0/5 * * * * *")) + .doNotScheduleOnStartup() // just for demo-purposes, so we can start it on-demand + .executeStateful((TaskInstance taskInstance, ExecutionContext executionContext) -> { + EventLogger.logTask(STATE_TRACKING_RECURRING_TASK, "Ran recurring task. Will keep running according to the same schedule, " + + "but the state is updated. State: " + taskInstance.getData()); + + // Stateful recurring return the updated state as the final step (convenience) + return taskInstance.getData() + 1; + }); + } + +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/SchedulerConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/SchedulerConfiguration.java new file mode 100644 index 00000000..cd65d95c --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/SchedulerConfiguration.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.kagkarlsson.scheduler.SchedulerName; +import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer; +import com.github.kagkarlsson.scheduler.serializer.JacksonSerializer; +import com.github.kagkarlsson.scheduler.serializer.Serializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +@Configuration +public class SchedulerConfiguration { + + /** + * Bean defined when a configuration-property in DbSchedulerCustomizer needs to be overridden. + */ + @Bean + DbSchedulerCustomizer customizer() { + return new DbSchedulerCustomizer() { + @Override + public Optional schedulerName() { + return Optional.of(new SchedulerName.Fixed("spring-boot-scheduler-1")); + } + + @Override + public Optional serializer() { + return Optional.of(new JacksonSerializer()); + } + }; + } + +} diff --git a/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TransactionallyStagedJobConfiguration.java b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TransactionallyStagedJobConfiguration.java new file mode 100644 index 00000000..74b4a255 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/com/github/kagkarlsson/examples/boot/config/TransactionallyStagedJobConfiguration.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.kagkarlsson.examples.boot.config; + +import com.github.kagkarlsson.examples.boot.ExampleContext; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.Task; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import com.github.kagkarlsson.scheduler.task.TaskWithoutDataDescriptor; +import com.github.kagkarlsson.scheduler.task.helper.Tasks; +import org.springframework.transaction.TransactionStatus; +import utils.EventLogger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Random; + +@Configuration +public class TransactionallyStagedJobConfiguration { + + public static final TaskWithoutDataDescriptor TRANSACTIONALLY_STAGED_TASK = new TaskWithoutDataDescriptor("transactionally-staged-task"); + private static int ID = 1; + + + /** Start the example */ + public static void start(ExampleContext ctx) { + ctx.log("Scheduling a one-time task in a transaction. If the transaction rolls back, the insert of the task also " + + "rolls back, i.e. it will never run." + ); + + ctx.tx.executeWithoutResult((TransactionStatus status) -> { + // Since it is scheduled in a transaction, the scheduler will not run it until the tx commits + // If the tx rolls back, the insert of the new job will also roll back, i.e. not run. + ctx.schedulerClient.schedule( + TRANSACTIONALLY_STAGED_TASK.instance(String.valueOf(ID++)), + Instant.now() + ); + + // Do additional database-operations here + + if (new Random().nextBoolean()) { + throw new RuntimeException("Simulated failure happening after task was scheduled. Scheduled task will never run."); + } + }); + } + + /** Bean definition */ + @Bean + public Task transactionallyStagedTask() { + return Tasks.oneTime(TRANSACTIONALLY_STAGED_TASK) + .execute((TaskInstance taskInstance, ExecutionContext executionContext) -> { + EventLogger.logTask(TRANSACTIONALLY_STAGED_TASK, "Ran. Will only run if transactions it was scheduled commits. "); + }); + } + +} diff --git a/examples/spring-boot-example/src/main/java/utils/EventLogger.java b/examples/spring-boot-example/src/main/java/utils/EventLogger.java new file mode 100644 index 00000000..fe761022 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/utils/EventLogger.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package utils; + +import com.github.kagkarlsson.scheduler.task.HasTaskName; +import com.github.kagkarlsson.scheduler.task.HasTaskName.SimpleTaskName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EventLogger { + + public static final Logger EVENT_LOG = LoggerFactory.getLogger("SchedulerEvents"); + + public static void logTask(String taskName, String message) { + logTask(new SimpleTaskName(taskName), message); + } + public static void logTask(HasTaskName taskName, String message) { + EVENT_LOG.info(taskName.getTaskName() + ": " + message); + } +} diff --git a/examples/spring-boot-example/src/main/java/utils/Utils.java b/examples/spring-boot-example/src/main/java/utils/Utils.java new file mode 100644 index 00000000..975f2c6e --- /dev/null +++ b/examples/spring-boot-example/src/main/java/utils/Utils.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) Gustav Karlsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package utils; + +import java.util.Random; + +public class Utils { + + public static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/examples/spring-boot-example/src/main/resources/application.properties b/examples/spring-boot-example/src/main/resources/application.properties index 661b6f88..90bf6283 100644 --- a/examples/spring-boot-example/src/main/resources/application.properties +++ b/examples/spring-boot-example/src/main/resources/application.properties @@ -5,15 +5,15 @@ spring.jpa.open-in-view=false management.endpoints.web.exposure.include=prometheus -# Enable debug output for db-scheduler, just to visualize what's going on -logging.level.com.github.kagkarlsson.scheduler=DEBUG +# Set lever to 'DEBUG' to see what goes on +logging.level.com.github.kagkarlsson.scheduler=INFO # Db-scheduler configuration db-scheduler.threads=5 db-scheduler.polling-interval=5s +db-scheduler.immediate-execution-enabled=true db-scheduler.polling-strategy=fetch db-scheduler.polling-strategy-lower-limit-fraction-of-threads=0.5 db-scheduler.polling-strategy-upper-limit-fraction-of-threads=3.0 - db-scheduler.shutdown-max-wait=30m diff --git a/examples/spring-boot-example/src/test/java/com/github/kagkarlsson/examples/boot/SmokeTest.java b/examples/spring-boot-example/src/test/java/com/github/kagkarlsson/examples/boot/SmokeTest.java index bf43e7ef..ea13f50f 100644 --- a/examples/spring-boot-example/src/test/java/com/github/kagkarlsson/examples/boot/SmokeTest.java +++ b/examples/spring-boot-example/src/test/java/com/github/kagkarlsson/examples/boot/SmokeTest.java @@ -59,9 +59,7 @@ public void it_should_have_a_scheduler_bean() { @Test public void it_should_have_two_tasks_exposed_as_beans() { - assertThat(ctx.getBeansOfType(Task.class).values()).hasSize(2); - assertThat(ctx.getBeansOfType(OneTimeTask.class).values()).hasSize(1); - assertThat(ctx.getBeansOfType(RecurringTask.class).values()).hasSize(1); + assertThat(ctx.getBeansOfType(Task.class).values()).hasSizeGreaterThan(10); } @Test diff --git a/pom.xml b/pom.xml index 1202fc55..ae5d58e8 100644 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,7 @@ **/*.sql .github/** test/** + **/RuntimeTypeAdapterFactory.java ${failOnMissingHeader}