/**
* 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 org.apache.aurora.scheduler.cron.quartz;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import org.apache.aurora.common.testing.easymock.EasyMockTest;
import org.apache.aurora.common.util.BackoffHelper;
import org.apache.aurora.gen.AssignedTask;
import org.apache.aurora.gen.CronCollisionPolicy;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.scheduler.BatchWorker;
import org.apache.aurora.scheduler.BatchWorker.RepeatableWork;
import org.apache.aurora.scheduler.base.JobKeys;
import org.apache.aurora.scheduler.cron.quartz.AuroraCronJob.CronBatchWorker;
import org.apache.aurora.scheduler.state.StateChangeResult;
import org.apache.aurora.scheduler.state.StateManager;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.Storage.MutateWork.NoResult;
import org.apache.aurora.scheduler.storage.db.DbUtil;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.easymock.Capture;
import org.junit.Before;
import org.junit.Test;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.impl.JobDetailImpl;
import static org.apache.aurora.scheduler.cron.quartz.QuartzTestUtil.AURORA_JOB_KEY;
import static org.apache.aurora.scheduler.testing.BatchWorkerUtil.expectBatchExecute;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class AuroraCronJobTest extends EasyMockTest {
private static final String TASK_ID = "A";
private JobDetailImpl jobDetails;
private Storage storage;
private StateManager stateManager;
private BackoffHelper backoffHelper;
private CronBatchWorker batchWorker;
private JobExecutionContext context;
private AuroraCronJob auroraCronJob;
@Before
public void setUp() throws Exception {
storage = DbUtil.createStorage();
stateManager = createMock(StateManager.class);
backoffHelper = createMock(BackoffHelper.class);
context = createMock(JobExecutionContext.class);
jobDetails = new JobDetailImpl();
jobDetails.setKey(Quartz.jobKey(AURORA_JOB_KEY));
jobDetails.setJobDataMap(new JobDataMap(new HashMap()));
expect(context.getJobDetail()).andReturn(jobDetails).anyTimes();
batchWorker = createMock(CronBatchWorker.class);
expectBatchExecute(batchWorker, storage, control).anyTimes();
auroraCronJob = new AuroraCronJob(
new AuroraCronJob.Config(backoffHelper),
stateManager,
batchWorker);
}
@Test
public void testExecuteNonexistentIsNoop() throws JobExecutionException {
control.replay();
auroraCronJob.doExecute(context);
}
@Test
public void testEmptyStorage() throws JobExecutionException {
stateManager.insertPendingTasks(anyObject(), anyObject(), anyObject());
expectLastCall().times(3);
control.replay();
populateStorage(CronCollisionPolicy.CANCEL_NEW);
auroraCronJob.doExecute(context);
storage = DbUtil.createStorage();
populateStorage(CronCollisionPolicy.KILL_EXISTING);
auroraCronJob.doExecute(context);
storage = DbUtil.createStorage();
populateStorage(CronCollisionPolicy.RUN_OVERLAP);
auroraCronJob.doExecute(context);
}
@Test
public void testCancelNew() throws JobExecutionException {
control.replay();
populateTaskStore();
populateStorage(CronCollisionPolicy.CANCEL_NEW);
auroraCronJob.doExecute(context);
}
@Test
public void testOverlap() throws JobExecutionException {
control.replay();
populateTaskStore();
populateStorage(CronCollisionPolicy.RUN_OVERLAP);
auroraCronJob.doExecute(context);
}
@Test
public void testKillExisting() throws Exception {
Capture<RepeatableWork<BatchWorker.NoResult>> killCapture = createCapture();
CompletableFuture<BatchWorker.NoResult> killResult = new CompletableFuture<>();
expect(batchWorker.executeWithReplay(anyObject(), capture(killCapture))).andReturn(killResult);
expect(backoffHelper.getBackoffStrategy()).andReturn(null).anyTimes();
expect(stateManager.changeState(
anyObject(),
eq(TASK_ID),
eq(Optional.absent()),
eq(ScheduleStatus.KILLING),
eq(AuroraCronJob.KILL_AUDIT_MESSAGE)))
.andReturn(StateChangeResult.SUCCESS);
stateManager.insertPendingTasks(anyObject(), anyObject(), anyObject());
expectLastCall().times(2);
control.replay();
populateStorage(CronCollisionPolicy.KILL_EXISTING);
populateTaskStore();
auroraCronJob.doExecute(context);
storage.write(
(NoResult.Quiet) storeProvider -> storeProvider.getUnsafeTaskStore().deleteAllTasks());
storage.write((NoResult.Quiet) store -> killCapture.getValue().apply(store));
// Simulate a trigger in progress.
jobDetails.getJobDataMap().put(JobKeys.canonicalString(AURORA_JOB_KEY), null);
assertFalse(jobDetails.getJobDataMap().isEmpty());
// Attempt a concurrent run that must be rejected.
auroraCronJob.doExecute(context);
// Complete previous run and trigger another one.
killResult.complete(BatchWorker.NO_RESULT);
auroraCronJob.doExecute(context);
assertTrue(jobDetails.getJobDataMap().isEmpty());
}
@Test
public void testNoConcurrentRun() throws Exception {
jobDetails.getJobDataMap().put(JobKeys.canonicalString(AURORA_JOB_KEY), null);
control.replay();
auroraCronJob.doExecute(context);
}
private void populateTaskStore() {
storage.write((NoResult.Quiet)
storeProvider -> storeProvider.getUnsafeTaskStore().saveTasks(ImmutableSet.of(
IScheduledTask.build(new ScheduledTask()
.setStatus(ScheduleStatus.RUNNING)
.setAssignedTask(new AssignedTask()
.setTaskId(TASK_ID)
.setTask(QuartzTestUtil.JOB.getTaskConfig().newBuilder()))))));
}
private void populateStorage(CronCollisionPolicy policy) {
storage.write((NoResult.Quiet) storeProvider -> storeProvider.getCronJobStore().saveAcceptedJob(
QuartzTestUtil.makeSanitizedCronJob(policy).getSanitizedConfig().getJobConfig()));
}
}