package de.otto.edison.jobs.service;
import de.otto.edison.jobs.definition.JobDefinition;
import de.otto.edison.jobs.domain.JobInfo;
import de.otto.edison.jobs.domain.JobMessage;
import de.otto.edison.jobs.domain.Level;
import de.otto.edison.jobs.eventbus.JobEventPublisher;
import de.otto.edison.jobs.repository.JobBlockedException;
import de.otto.edison.jobs.repository.JobRepository;
import de.otto.edison.status.domain.SystemInfo;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.boot.actuate.metrics.GaugeService;
import org.springframework.context.ApplicationEventPublisher;
import java.time.Clock;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static de.otto.edison.jobs.definition.DefaultJobDefinition.manuallyTriggerableJobDefinition;
import static de.otto.edison.jobs.domain.JobInfo.newJobInfo;
import static de.otto.edison.jobs.domain.JobMessage.jobMessage;
import static de.otto.edison.status.domain.SystemInfo.systemInfo;
import static java.time.Clock.fixed;
import static java.time.Clock.offset;
import static java.time.Instant.now;
import static java.time.ZoneId.systemDefault;
import static java.time.temporal.ChronoUnit.MINUTES;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
public class JobServiceTest {
private static final String HOSTNAME = "HOST";
private static final String JOB_ID = "JOB/ID";
private static final String JOB_TYPE = "JOB_TYPE";
@Mock
private ScheduledExecutorService executorService;
@Mock
private ApplicationEventPublisher applicationEventPublisher;
@Mock
private JobRunnable jobRunnable;
@Mock
private JobRepository jobRepository;
@Mock
private GaugeService gaugeServiceMock;
@Mock
private UuidProvider uuidProviderMock;
@Mock
private JobMetaService jobMetaService;
private JobService jobService;
private SystemInfo systemInfo;
private Clock clock;
@Before
@SuppressWarnings("unchecked")
public void setUp() throws Exception {
initMocks(this);
this.systemInfo = systemInfo(HOSTNAME, 8080);
this.clock = fixed(now(), systemDefault());
doAnswer(new RunImmediately())
.when(executorService)
.execute(any(Runnable.class));
when(executorService.scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class)))
.thenReturn(mock(ScheduledFuture.class));
when(jobRunnable.getJobDefinition())
.thenReturn(manuallyTriggerableJobDefinition("someType", "bla", "bla", 0, Optional.empty()));
when(uuidProviderMock.getUuid())
.thenReturn(JOB_ID);
jobService = new JobService(
jobRepository, jobMetaService, singletonList(jobRunnable),
gaugeServiceMock, executorService, applicationEventPublisher, clock, systemInfo, uuidProviderMock);
jobService.postConstruct();
}
@Test
public void shouldReturnCreatedJobId() {
// given:
when(jobRunnable.getJobDefinition()).thenReturn(someJobDefinition("BAR"));
// when:
Optional<String> jobId = jobService.startAsyncJob("BAR");
// then:
assertThat(jobId.isPresent(), is(true));
assertThat(jobId.get(), not(isEmptyOrNullString()));
}
@Test
public void shouldRunJob() {
// given:
String jobType = "bar";
when(jobRunnable.getJobDefinition()).thenReturn(someJobDefinition(jobType));
// when:
Optional<String> optionalJobId = jobService.startAsyncJob(jobType);
// then:
final JobInfo expectedJobInfo = JobInfo.newJobInfo(optionalJobId.get(), jobType, clock, systemInfo.hostname);
verify(executorService).execute(any(Runnable.class));
verify(jobRepository).createOrUpdate(expectedJobInfo);
verify(jobRunnable).execute(any(JobEventPublisher.class));
verify(jobMetaService).aquireRunLock(expectedJobInfo.getJobId(), expectedJobInfo.getJobType());
}
@Test
public void shouldNotStartJobOnBlockedException() {
doAnswer((x) -> {throw new JobBlockedException("");})
.when(jobMetaService).aquireRunLock(anyString(), anyString());
Optional<String> jobUri = jobService.startAsyncJob("someType");
assertThat(jobUri.isPresent(), is(false));
verify(jobRepository, never()).createOrUpdate(any());
}
@Test
public void shouldReportRuntime() {
// given:
when(jobRunnable.getJobDefinition()).thenReturn(someJobDefinition("BAR"));
// when:
jobService.startAsyncJob("BAR");
// then:
verify(gaugeServiceMock).submit(eq("gauge.jobs.runtime.bar"), anyLong());
}
@Test
public void shouldStopJob() {
OffsetDateTime now = OffsetDateTime.now(clock);
Clock earlierClock = offset(clock, Duration.of(-1, MINUTES));
JobInfo jobInfo = JobInfo.newJobInfo("superId", "superType", earlierClock, HOSTNAME);
when(jobRepository.findOne("superId")).thenReturn(Optional.of(jobInfo));
jobService.stopJob("superId");
JobInfo expected = jobInfo.copy().setStatus(JobInfo.JobStatus.OK).setStopped(now).setLastUpdated(now).build();
verify(jobMetaService).releaseRunLock("superType");
verify(jobRepository).createOrUpdate(expected);
}
@Test
public void shouldKillJob() {
OffsetDateTime now = OffsetDateTime.now(clock);
JobInfo jobInfo = JobInfo.newJobInfo("superId", "superType", clock, HOSTNAME);
when(jobRepository.findOne("superId")).thenReturn(Optional.of(jobInfo));
jobService.killJob("superId", "superType");
JobInfo expected = jobInfo.copy().setStatus(JobInfo.JobStatus.DEAD).setStopped(now).setLastUpdated(now).build();
verify(jobMetaService).releaseRunLock("superType");
verify(jobRepository).createOrUpdate(expected);
}
@Test
public void shouldKillDeadJobsSince() {
JobInfo someJobInfo = defaultJobInfo().setJobType("jobType").build();
when(jobRepository.findRunningWithoutUpdateSince(any())).thenReturn(singletonList(someJobInfo));
when(jobRepository.findOne(someJobInfo.getJobId())).thenReturn(Optional.of(someJobInfo));
jobService.killJobsDeadSince(60);
verify(jobMetaService).releaseRunLock("jobType");
}
@Test
public void shouldUpdateTimeStampOnKeepAlive() {
//when
jobService.keepAlive(JOB_ID);
//then
OffsetDateTime now = OffsetDateTime.now(clock);
verify(jobRepository).setLastUpdate(JOB_ID, now);
}
@Test
public void shouldMarkSkipped() {
//when
jobService.markSkipped(JOB_ID);
// then
OffsetDateTime now = OffsetDateTime.now(clock);
verify(jobRepository).appendMessage(JOB_ID, jobMessage(Level.INFO, "Skipped job ..", now));
verify(jobRepository).setLastUpdate(JOB_ID, now);
verify(jobRepository).setJobStatus(JOB_ID, JobInfo.JobStatus.SKIPPED);
}
@Test
public void shouldMarkRestarted() {
//when
jobService.markRestarted(JOB_ID);
// then
OffsetDateTime now = OffsetDateTime.now(clock);
verify(jobRepository).appendMessage(JOB_ID, jobMessage(Level.WARNING, "Restarting job ..", now));
verify(jobRepository).setLastUpdate(JOB_ID, now);
verify(jobRepository).setJobStatus(JOB_ID, JobInfo.JobStatus.OK);
}
@Test
public void shouldAppendNonErrorMessage() {
JobMessage message = JobMessage.jobMessage(Level.INFO, "This is an interesting message", OffsetDateTime.now());
// when
jobService.appendMessage(JOB_ID, message);
// then
verify(jobRepository).appendMessage(JOB_ID, message);
verify(jobRepository, never()).createOrUpdate(any(JobInfo.class));
}
@Test
public void shouldAppendErrorMessageAndSetErrorStatus() {
OffsetDateTime now = OffsetDateTime.now(clock);
OffsetDateTime earlier = now.minus(10, MINUTES);
JobMessage message = JobMessage.jobMessage(Level.ERROR, "Error: Out of hunk", now);
JobInfo jobInfo = defaultJobInfo()
.setLastUpdated(earlier)
.build();
when(jobRepository.findOne(JOB_ID)).thenReturn(Optional.of(jobInfo));
// when
jobService.appendMessage(JOB_ID, message);
// then
JobInfo expected = jobInfo.copy()
.setStatus(JobInfo.JobStatus.ERROR)
.setLastUpdated(now).build();
verify(jobRepository).appendMessage(JOB_ID, message);
verify(jobRepository).createOrUpdate(expected);
}
private JobInfo.Builder defaultJobInfo() {
return newJobInfo(JOB_ID, JOB_TYPE, clock, HOSTNAME).copy();
}
private JobDefinition someJobDefinition(String jobType) {
return new JobDefinition() {
@Override
public String jobType() {
return jobType;
}
@Override
public String jobName() {
return "test";
}
@Override
public String description() {
return "test";
}
};
}
private static class RunImmediately implements Answer<Object> {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Runnable runnable = (Runnable) invocation.getArguments()[0];
runnable.run();
return null;
}
}
}