Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static Cron cron(String expression){
}

static Interval interval(Duration interval){
return new Interval(interval);
return new Interval(interval, TimeRange.infinite());
}

static Fixed fixed(Instant at){
Expand Down Expand Up @@ -44,20 +44,39 @@ public String scheduleExpression() {
* @param interval A positive Duration object that represents the time interval between each job execution.
* At least 1 second
*/
record Interval(Duration interval) implements Schedule {
record Interval(Duration interval, TimeRange timeRange) implements Schedule {
public Interval {
if (interval.isNegative()) {
throw new IllegalArgumentException("Interval must be positive");
}
if (interval.getSeconds() < 1) {
throw new IllegalArgumentException("Interval must be at least 1 second");
}
if(timeRange == null){
throw new IllegalArgumentException("Interval must have a specified time range in which it will be active");
}
}

@Override
public String scheduleExpression() {
return "@every %ss".formatted(interval.getSeconds());
}

/**
* @param startInstant date after which Schedule will be active
* @return new Schedule Interval with a start date instant
*/
public Interval startAfter(Instant startInstant){
return new Interval(interval, timeRange.withStart(startInstant));
}

/**
* @param endInstant date after which Schedule will NOT be active
* @return new Schedule Interval with an end date instant
*/
public Interval endBefore(Instant endInstant){
return new Interval(interval, timeRange.withEnd(endInstant));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.bloowper.schedulerclient.api;

import org.jetbrains.annotations.Nullable;

import java.time.Instant;
import java.util.Optional;

public class TimeRange {
@Nullable
private final Instant start;
@Nullable
private final Instant end;

public TimeRange(@Nullable Instant start, @Nullable Instant end) {
if(start != null && end != null && start.isAfter(end)) {
throw new IllegalArgumentException("Start have to be smaller than end");
}
this.start = start;
this.end = end;
}

TimeRange withStart(Instant start) {
return new TimeRange(start, this.end);
}

TimeRange withEnd(Instant end) {
return new TimeRange(this.start, end);
}

static TimeRange start(Instant start){
return new TimeRange(start, null);
}

static TimeRange end(Instant end){
return new TimeRange(null, end);
}

static TimeRange infinite(){
return new TimeRange(null, null);
}

Optional<Instant> getStart(){
return Optional.ofNullable(start);
}

Optional<Instant> getEnd(){
return Optional.ofNullable(end);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import static org.mockito.Mockito.mock;

class SharedTestInitializer {
public class SharedTestInitializer {

protected static final Instant NOW = Instant.parse("2021-01-01T00:00:00.00Z");
protected static final Clock CLOCK = Clock.fixed(NOW, ZoneOffset.UTC);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.github.bloowper.schedulerclient.api;


import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

class IntervalScheduleTest {
public static final Instant EARLY_INSTANT = Instant.parse("2021-01-01T00:00:00Z");

@Test
void shouldCreateIntervalSchedule() {
// Given
Duration interval = Duration.ofSeconds(60);

// When
Schedule.Interval schedule = Schedule.interval(interval);

// Then
assertEquals("@every 60s", schedule.scheduleExpression());
}

@Test
void intervalScheduleCreationShouldFailedOnToShortDuration() {
// Given
Duration interval = Duration.ofMillis(250);

// Then
assertThatThrownBy(() -> Schedule.interval(interval))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Interval must be at least 1 second");
}

@Test
void intervalScheduleCreationShouldFailedOnNegativeDuration() {
// Given
Duration interval = Duration.ofSeconds(-60);

// Then
assertThatThrownBy(() -> Schedule.interval(interval))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Interval must be positive");
}

@Test
void shouldAddStartInstantToInterval() {
// Given
Schedule.Interval schedule = Schedule.interval(Duration.ofSeconds(60));

// When
Schedule.Interval scheduleWithStartInstant = schedule.startAfter(EARLY_INSTANT);

// Then
assertEquals(scheduleWithStartInstant.timeRange().getStart().get(), EARLY_INSTANT);
}

@Test
void shouldAddEndInstantToInterval() {
// Given
Schedule.Interval schedule = Schedule.interval(Duration.ofSeconds(60));

// When
Schedule.Interval scheduleWithEndInstant = schedule.endBefore(EARLY_INSTANT);

// Then
assertEquals(scheduleWithEndInstant.timeRange().getEnd().get(), EARLY_INSTANT);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.github.bloowper.schedulerclient;
package com.github.bloowper.schedulerclient.api;

import com.github.bloowper.schedulerclient.api.JobDescription;
import com.github.bloowper.schedulerclient.api.JobId;
import com.github.bloowper.schedulerclient.api.Schedule;
import com.github.bloowper.schedulerclient.SharedTestInitializer;
import org.junit.jupiter.api.Test;

import java.time.Instant;

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

class JobSchedulerTest extends SharedTestInitializer {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.github.bloowper.schedulerclient.common;

import lombok.Getter;

import java.time.Duration;
import java.time.Instant;

@Getter
public class TestStopper {
private final Instant start;
private Instant end;

public TestStopper(Instant start) {
this.start = start;
}

public static TestStopper start(){
return new TestStopper(Instant.now());
}

public TestStopper stop(){
end = Instant.now();
return this;
}

public Duration getElapsedTime(){
return Duration.between(start, end);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,28 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

@Slf4j
class JobExecutionListener {
private List<DummyNotificationDto> dummyNotificationDtos = new ArrayList<>();
final private List<DummyNotificationDto> dummyNotificationDtos = new ArrayList<>();
final private List<Instant> executionDates = new ArrayList<>(30);
@EventListener
void onJobExecution(DummyNotificationDto dummyNotificationDto) {
log.info("Received notification: {}", dummyNotificationDto);
Instant now = Instant.now();
log.info("Received notification: {} at: {}", dummyNotificationDto,now);
dummyNotificationDtos.add(dummyNotificationDto);
executionDates.add(now);
}

public Boolean wasExecuted(DummyNotificationDto dummyNotificationDto, Integer times) {
public Boolean wasExecutedWith(DummyNotificationDto dummyNotificationDto, Integer times) {
return dummyNotificationDtos.stream().filter(dummyNotificationDto::equals).count() == times;
}

public void resetExecutionStatus() {
dummyNotificationDtos = new ArrayList<>();
public Boolean wasExecutedWith(DummyNotificationDto dummyNotificationDto){
return dummyNotificationDtos.stream().anyMatch(dummyNotificationDto::equals);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,108 @@
import com.github.bloowper.schedulerclient.api.JobId;
import com.github.bloowper.schedulerclient.api.JobScheduler;
import com.github.bloowper.schedulerclient.api.Schedule;
import com.github.bloowper.schedulerclient.common.TestStopper;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;

import java.time.Duration;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;


class SchedulingExecutionIT extends IntegrationTestBase {
// Uses default JonExecutionNotifier
private static final Duration ALLOWED_DIFFERENCE = Duration.ofMillis(950);
// Uses default JonExecutionNotifier
@Autowired
private JobScheduler jobScheduler;
@Autowired
JobExecutionListener jobExecutionListener;

// TODO: better handling of job execution listener then @DirtyContext
@Test
@DirtiesContext
void createFixedJobAndAwaitForExecution() {
@SneakyThrows
void fixedJobShouldBeExecuted() {
// Given
DummyNotificationDto notificationDto = new DummyNotificationDto("dummyValue1");
System.out.println("notificationDto: " + notificationDto);
JobId id = JobId.autoGenerated();
Instant now = Instant.now();
TestStopper stopper = TestStopper.start();
Duration delay = Duration.ofSeconds(30);
DummyNotificationDto dummyNotificationDto = new DummyNotificationDto("fixed job should not be called before execution date");
JobDescription jobDescription = new JobDescription(
id,
notificationDto,
Schedule.fixed(now.plus(Duration.ofSeconds(3)))
dummyNotificationDto,
Schedule.fixed(stopper.getStart().plus(delay))
);

// When
jobScheduler.scheduleJob(jobDescription);
JobId jobId = jobScheduler.scheduleJob(jobDescription);

// Then
await().atMost(Duration.ofSeconds(30)).until(() -> jobExecutionListener.wasExecuted(notificationDto,1));
await().atMost(delay).until(() -> jobExecutionListener.wasExecutedWith(dummyNotificationDto,1));
stopper.stop();
Duration elapsedTime = stopper.getElapsedTime();
assertThat(elapsedTime).isCloseTo(delay, ALLOWED_DIFFERENCE);
}


@Test
@DirtiesContext
void createIntervalJobAndAwaitForExecution() {
void createCronJobAndAwaitForExecution() {
// Given
DummyNotificationDto notificationDto = new DummyNotificationDto("dummyValue2");
System.out.println("notificationDto: " + notificationDto);
DummyNotificationDto notificationDto = new DummyNotificationDto("dummyValue3");

JobId id = JobId.autoGenerated();
JobDescription jobDescription = new JobDescription(
id,
notificationDto,
Schedule.interval(Duration.ofSeconds(3))
Schedule.cron("*/4 * * * * *")
);

// When
jobScheduler.scheduleJob(jobDescription);

// Then
await().atMost(Duration.ofSeconds(30)).until(() -> jobExecutionListener.wasExecuted(notificationDto,2));
await().atMost(Duration.ofSeconds(30)).until(() -> jobExecutionListener.wasExecutedWith(notificationDto,1));
}

@Test
@DirtiesContext
void createCronJobAndAwaitForExecution() {
void createIntervalJobAndAwaitForExecutions() {
// Given
DummyNotificationDto notificationDto = new DummyNotificationDto("dummyValue3");
System.out.println("notificationDto: " + notificationDto);
DummyNotificationDto notificationDto = new DummyNotificationDto("interval job execution test");

JobId id = JobId.autoGenerated();
JobDescription jobDescription = new JobDescription(
id,
notificationDto,
Schedule.cron("*/4 * * * * *")
Schedule.interval(Duration.ofSeconds(3))
);

// When
jobScheduler.scheduleJob(jobDescription);

// Then
await().atMost(Duration.ofSeconds(30)).until(() -> jobExecutionListener.wasExecuted(notificationDto,1));
await().atMost(Duration.ofSeconds(30)).until(() -> jobExecutionListener.wasExecutedWith(notificationDto,2));
}

@Test
void createIntervalJobWithDelayAndAwaitForExecution(){
// Given
TestStopper stopper = TestStopper.start();
Instant now = stopper.getStart();
Duration interval = Duration.ofSeconds(1);
Duration delay = Duration.ofSeconds(10);
DummyNotificationDto notificationDto = new DummyNotificationDto("job that should be called every second after given instant");
Schedule.Interval schedule = Schedule.interval(interval).startAfter(now.plus(delay)); // Job that run every 1 second but after 10-second delay
JobDescription jobDescription = new JobDescription(notificationDto, schedule);

// When
jobScheduler.scheduleJob(jobDescription);

// Then
await().atMost(delay).until(() -> jobExecutionListener.wasExecutedWith(notificationDto));
Duration elapsedTime = stopper.stop().getElapsedTime();
assertThat(elapsedTime).isCloseTo(delay, ALLOWED_DIFFERENCE);
}
}