diff --git a/prototype/src/main/java/org/hso/ecommerce/action/cronjob/ICronjob.java b/prototype/src/main/java/org/hso/ecommerce/action/cronjob/ICronjob.java new file mode 100644 index 0000000..bf2f3d8 --- /dev/null +++ b/prototype/src/main/java/org/hso/ecommerce/action/cronjob/ICronjob.java @@ -0,0 +1,29 @@ +package org.hso.ecommerce.action.cronjob; + +import java.util.Calendar; + +public interface ICronjob { + /** + * Calculate the earliest cronjob execution time that happens after the given reference time. + * + * @param reference Position in time to start searching. The implementor is allowed to modify the reference time. + * @return A new Calendar instance (or the same) containing the time for next execution. + */ + Calendar nextExecution(Calendar reference); + + /** + * Calculate the latest cronjob execution time that happens before or exactly at the given refernce time. + * + * @param reference Position in time to start searching. The implementor is allowed to modify the reference time. + * @return A new Calendar instance (or the same) containing the time of the last execution. + */ + Calendar previousExecution(Calendar reference); + + /** + * Execute this cronjob. + * + * @param time The point in time this execution was scheduled. In case of a missed cronjob, the actual time of this + * call might be much later. + */ + void executeAt(Calendar time); +} diff --git a/prototype/src/main/java/org/hso/ecommerce/action/cronjob/Reorder.java b/prototype/src/main/java/org/hso/ecommerce/action/cronjob/Reorder.java new file mode 100644 index 0000000..212f696 --- /dev/null +++ b/prototype/src/main/java/org/hso/ecommerce/action/cronjob/Reorder.java @@ -0,0 +1,38 @@ +package org.hso.ecommerce.action.cronjob; + +import java.util.Calendar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Reorder implements ICronjob { + private static final Logger log = LoggerFactory.getLogger(Reorder.class); + + @Override + public Calendar nextExecution(Calendar reference) { + if (reference.get(Calendar.HOUR_OF_DAY) >= 8) { + reference.add(Calendar.DAY_OF_MONTH, 1); + } + reference.set(Calendar.HOUR_OF_DAY, 8); + reference.set(Calendar.MINUTE, 0); + reference.set(Calendar.SECOND, 0); + reference.set(Calendar.MILLISECOND, 0); + return reference; + } + + @Override + public Calendar previousExecution(Calendar reference) { + if (reference.get(Calendar.HOUR_OF_DAY) < 8) { + reference.add(Calendar.DAY_OF_MONTH, -1); + } + reference.set(Calendar.HOUR_OF_DAY, 8); + reference.set(Calendar.MINUTE, 0); + reference.set(Calendar.SECOND, 0); + reference.set(Calendar.MILLISECOND, 0); + return reference; + } + + @Override + public void executeAt(Calendar time) { + log.info("Executing Reorder Cronjob"); + } +} diff --git a/prototype/src/main/java/org/hso/ecommerce/controller/cronjob/CronjobController.java b/prototype/src/main/java/org/hso/ecommerce/controller/cronjob/CronjobController.java new file mode 100644 index 0000000..4f58c58 --- /dev/null +++ b/prototype/src/main/java/org/hso/ecommerce/controller/cronjob/CronjobController.java @@ -0,0 +1,117 @@ +package org.hso.ecommerce.controller.cronjob; + +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import javax.annotation.PostConstruct; +import org.hso.ecommerce.action.cronjob.ICronjob; +import org.hso.ecommerce.action.cronjob.Reorder; +import org.hso.ecommerce.entities.cron.BackgroundJob; +import org.hso.ecommerce.repos.cronjob.BackgroundJobRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +class ScheduledCronjob { + public final Calendar executionTime; + public final ICronjob cronjob; + public final BackgroundJob model; + + public ScheduledCronjob(Calendar executionTime, ICronjob cronjob, BackgroundJob model) { + this.executionTime = executionTime; + this.cronjob = cronjob; + this.model = model; + } +} + +@Component +class CronjobController { + private static final Logger log = LoggerFactory.getLogger(CronjobController.class); + + private static final Map cronjobs = getCronjobs(); + + @Autowired + private final BackgroundJobRepository cronjobRepository = null; + + private static Map getCronjobs() { + HashMap map = new HashMap<>(); + + // Register all existing cronjobs + map.put(BackgroundJob.JOB_REORDER, new Reorder()); + + return Collections.unmodifiableMap(map); + } + + private ScheduledCronjob getNextCronjob() { + Calendar currentTime = new GregorianCalendar(); + Iterable jobs = cronjobRepository.getAllJobs(); + HashMap alreadyExecuted = new HashMap<>(); + for (BackgroundJob job : jobs) { + alreadyExecuted.put(job.jobName, job); + } + ScheduledCronjob earliestJob = null; + for (Entry entry : cronjobs.entrySet()) { + ScheduledCronjob resultingJob; + BackgroundJob dbEntry = alreadyExecuted.get(entry.getKey()); + if (dbEntry != null) { + Calendar previousExecution = new GregorianCalendar(); + previousExecution.setTimeInMillis(dbEntry.lastExecution.getTime()); + Calendar followingSchedule = entry.getValue().nextExecution((Calendar) previousExecution.clone()); + Calendar lastSchedule = entry.getValue().previousExecution((Calendar) currentTime.clone()); + if (lastSchedule.getTimeInMillis() > followingSchedule.getTimeInMillis()) { + // This happens, if more than one execution was missed. + // In this case, run the job only once. + followingSchedule = lastSchedule; + } + resultingJob = new ScheduledCronjob(followingSchedule, entry.getValue(), dbEntry); + } else { + // This cronjob has never been executed before. + Calendar lastScheduleTime = entry.getValue().previousExecution((Calendar) currentTime.clone()); + BackgroundJob model = new BackgroundJob(); + model.jobName = entry.getKey(); + resultingJob = new ScheduledCronjob(lastScheduleTime, entry.getValue(), model); + } + + // Look for the job with earliest executionTime - it will run next + if (earliestJob == null + || resultingJob.executionTime.getTimeInMillis() < earliestJob.executionTime.getTimeInMillis()) { + earliestJob = resultingJob; + } + } + return earliestJob; + } + + private void runCronjobExecutionLoop() { + Thread.currentThread().setName("Cronjob"); + try { + while (true) { + ScheduledCronjob nextJob = getNextCronjob(); + if (nextJob == null) { + // In case there are no cronjobs + return; + } + long waitingTime = nextJob.executionTime.getTimeInMillis() - System.currentTimeMillis(); + if (waitingTime > 0) { + Thread.sleep(waitingTime); + } + + nextJob.cronjob.executeAt(nextJob.executionTime); + + nextJob.model.lastExecution = new Timestamp(nextJob.executionTime.getTimeInMillis()); + cronjobRepository.save(nextJob.model); + } + } catch (InterruptedException e) { + log.error("The cronjob execution thread has been interrupted"); + } + } + + @PostConstruct + public void onPostConstruct() { + new Thread(this::runCronjobExecutionLoop).start(); + } +} diff --git a/prototype/src/main/java/org/hso/ecommerce/entities/cron/BackgroundJob.java b/prototype/src/main/java/org/hso/ecommerce/entities/cron/BackgroundJob.java index 14a4ae3..7b0e09e 100644 --- a/prototype/src/main/java/org/hso/ecommerce/entities/cron/BackgroundJob.java +++ b/prototype/src/main/java/org/hso/ecommerce/entities/cron/BackgroundJob.java @@ -7,8 +7,8 @@ import javax.validation.constraints.NotNull; @Table(name = "background_jobs") public class BackgroundJob { - public final String JOB_DASHBOARD = "Dashboard"; - public final String JOB_REORDER = "SupplierOrder"; + public static final String JOB_DASHBOARD = "Dashboard"; + public static final String JOB_REORDER = "SupplierOrder"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/prototype/src/main/java/org/hso/ecommerce/repos/cronjob/BackgroundJobRepository.java b/prototype/src/main/java/org/hso/ecommerce/repos/cronjob/BackgroundJobRepository.java new file mode 100644 index 0000000..a3aad66 --- /dev/null +++ b/prototype/src/main/java/org/hso/ecommerce/repos/cronjob/BackgroundJobRepository.java @@ -0,0 +1,12 @@ +package org.hso.ecommerce.repos.cronjob; + +import org.hso.ecommerce.entities.cron.BackgroundJob; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface BackgroundJobRepository extends JpaRepository { + @Query(value = "SELECT * FROM background_jobs", nativeQuery = true) + Iterable getAllJobs(); +}