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}