Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cron expression #17

Merged
merged 8 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,24 @@ the job will be executed once 10 seconds after it has been scheduled.

### Cron
Schedules can be created using [cron expressions](https://en.wikipedia.org/wiki/Cron#CRON_expression).
This feature is made possible by the use of [cron-utils](https://github.com/jmrozanec/cron-utils).
So to use cron expression, cron-utils should be added in the project:
This feature is made possible by the use of [cron library](https://github.com/frode-carlsen/cron). This library is very lightweight: it has no dependency and is made of a single Java class of 650 lines of code.

So to use cron expression, this library has to be added:
```xml
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.1.6</version>
<groupId>ch.eitchnet</groupId>
<artifactId>cron</artifactId>
<version>1.6.2</version>
</dependency>
```

Then to create a job which is executed every hour at the 30th minute,
you can create the schedule: `CronSchedule.parseQuartzCron("0 30 * * * ? *")`.
you can create the schedule: `CronExpressionSchedule.parse("30 * * * *")`.

Cron expression should be checked using a tool like [Cronhub](https://crontab.cronhub.io/).

Cron expression should be created and checked using a tool like [Cron Maker](http://www.cronmaker.com/).
Cron-utils was the default Cron implementation before Wisp 2.2.2. This has [changed in version 2.3.0](/../../issues/14).
Documentation about cron-utils implementation can be found at [Wisp 2.2.2](/../../tree/2.2.2#cron).

### Custom schedules
Custom schedules can be created,
Expand Down
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.coreoz</groupId>
<artifactId>wisp</artifactId>
<version>2.2.3-SNAPSHOT</version>
<version>2.3.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Wisp Scheduler</name>
Expand Down Expand Up @@ -176,6 +176,12 @@
<version>9.1.6</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ch.eitchnet</groupId>
<artifactId>cron</artifactId>
<version>1.6.2</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>junit</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.coreoz.wisp.schedule.cron;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import com.coreoz.wisp.schedule.Schedule;

import fc.cron.CronExpression;

/**
* A {@link Schedule} based on a <a href="https://en.wikipedia.org/wiki/Cron#CRON_expression">
* Cron expression</a>.<br>
* <br>
* This class depends on <a href="https://github.com/frode-carlsen/cron">Cron library</a>,
* so this dependency has to be in the classpath in order to be able to use {@link CronExpressionSchedule}.
* Since the Cron library is marked as optional in Wisp, it has to be
* <a href="https://github.com/Coreoz/Wisp#cron">explicitly referenced in the project dependency configuration</a>
* (pom.xml, build.gradle, build.sbt etc.).<br>
* <br>
* See also {@link CronExpression} for format details and implementation.
*/
public class CronExpressionSchedule implements Schedule {

private final CronExpression cronExpression;
private final ZoneId zoneId;

public CronExpressionSchedule(CronExpression cronExpression, ZoneId zoneId) {
this.cronExpression = cronExpression;
this.zoneId = zoneId;
}

public CronExpressionSchedule(CronExpression cronExpression) {
this(cronExpression, ZoneId.systemDefault());
}

@Override
public long nextExecutionInMillis(long currentTimeInMillis, int executionsCount, Long lastExecutionTimeInMillis) {
Instant currentInstant = Instant.ofEpochMilli(currentTimeInMillis);
try {
return cronExpression.nextTimeAfter(ZonedDateTime.ofInstant(
currentInstant,
zoneId
)).toEpochSecond() * 1000L;
} catch (IllegalArgumentException e) {
return Schedule.WILL_NOT_BE_EXECUTED_AGAIN;
}
}

@Override
public String toString() {
return cronExpression.toString();
}

/**
* Create a {@link Schedule} from a cron expression based on the Unix format,
* e.g. 1 * * * * for each minute.
*/
public static CronExpressionSchedule parse(String cronExpression) {
return new CronExpressionSchedule(CronExpression.createWithoutSeconds(cronExpression));
}

/**
* Create a {@link Schedule} from a cron expression based on the Unix format, but accepting a second field as the first one,
* e.g. 29 * * * * * for each minute at the second 29, for instance 12:05:29.
*/
public static CronExpressionSchedule parseWithSeconds(String cronExpression) {
return new CronExpressionSchedule(CronExpression.create(cronExpression));
}

}
12 changes: 12 additions & 0 deletions src/main/java/com/coreoz/wisp/schedule/cron/CronSchedule.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
* so this dependency have to be in the classpath in order to be able to use {@link CronSchedule}.
* Since cron-utils is marked as optional, it has to be explicitly referenced in the
* project dependency configuration (pom.xml, build.gradle, build.sbt etc.).
*
* @deprecated Use {@link CronExpressionScheduleTest} instead.
* This class has been deprecated to move away from cron-utils. See
* <a href="https://github.com/Coreoz/Wisp/issues/14">issue #14</a> for details.
*/
@Deprecated
public class CronSchedule implements Schedule {

private static final CronParser UNIX_CRON_PARSER = new CronParser(
Expand Down Expand Up @@ -68,15 +73,22 @@ public String toString() {
/**
* Create a {@link Schedule} from a cron expression based on the Unix format,
* e.g. 1 * * * * for each minute.
*
* @deprecated Use {@link CronExpressionScheduleTest#parse(String)} instead
*/
@Deprecated
public static CronSchedule parseUnixCron(String cronExpression) {
return new CronSchedule(UNIX_CRON_PARSER.parse(cronExpression));
}

/**
* Create a {@link Schedule} from a cron expression based on the Quartz format,
* e.g. 0 * * * * ? * for each minute.
*
* @deprecated Use {@link CronExpressionScheduleTest#parse(String)}
* or {@link CronExpressionScheduleTest#parseWithSeconds(String)} instead
*/
@Deprecated
public static CronSchedule parseQuartzCron(String cronExpression) {
return new CronSchedule(QUARTZ_CRON_PARSER.parse(cronExpression));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.coreoz.wisp.schedule.cron;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import org.junit.Test;

public class CronExpressionScheduleTest {

@Test
public void should_calcule_the_next_execution_time_based_on_a_unix_cron_expression() {
CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parse("* * * * *");

// To ease calculations, next execution time are calculated from the timestamp "0".
// So here, the absolute timestamp for an execution in 1 minute will be 60
assertThat(everyMinuteScheduler.nextExecutionInMillis(0, 0, null))
.isEqualTo(Duration.ofMinutes(1).toMillis());
}

@Test
public void should_calcule_the_next_execution_time_based_on_a_unix_cron_expression_with_seconds() {
CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parseWithSeconds("29 * * * * *");

assertThat(everyMinuteScheduler.nextExecutionInMillis(0, 0, null))
// the first iteration will be the absolute timestamp "29"
.isEqualTo(Duration.ofSeconds(29).toMillis());
assertThat(everyMinuteScheduler.nextExecutionInMillis(29000 , 1, null))
// the second iteration will be the absolute timestamp "89"
.isEqualTo(Duration.ofSeconds(60 + 29).toMillis());
}

@Test
public void should_not_executed_daily_jobs_twice_a_day() {
CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parse("0 12 * * *");

ZonedDateTime augustMidday = LocalDate
.of(2016, 8, 31)
.atTime(12, 0)
.atZone(ZoneId.systemDefault());
long midday = augustMidday.toEpochSecond() * 1000;

assertThat(everyMinuteScheduler.nextExecutionInMillis(midday-1, 0, null))
.isEqualTo(midday);
assertThat(everyMinuteScheduler.nextExecutionInMillis(midday, 0, null))
.isEqualTo(midday + Duration.ofDays(1).toMillis());
}

}