/*
* Copyright © 2014-2016 Cask Data, Inc.
*
* 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 co.cask.cdap.internal.app.runtime.schedule;
import co.cask.cdap.AppWithWorkflow;
import co.cask.cdap.api.app.ApplicationSpecification;
import co.cask.cdap.api.schedule.SchedulableProgramType;
import co.cask.cdap.api.schedule.Schedule;
import co.cask.cdap.api.schedule.ScheduleSpecification;
import co.cask.cdap.api.schedule.Schedules;
import co.cask.cdap.api.workflow.ScheduleProgramInfo;
import co.cask.cdap.app.store.Store;
import co.cask.cdap.common.namespace.NamespaceAdmin;
import co.cask.cdap.internal.AppFabricTestHelper;
import co.cask.cdap.internal.app.DefaultApplicationSpecification;
import co.cask.cdap.internal.schedule.TimeSchedule;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.proto.ProgramType;
import co.cask.cdap.proto.ScheduledRuntime;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.twill.filesystem.LocationFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.quartz.ObjectAlreadyExistsException;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class SchedulerServiceTest {
private static SchedulerService schedulerService;
private static Store store;
private static LocationFactory locationFactory;
private static NamespaceAdmin namespaceAdmin;
private static final Id.Namespace namespace = new Id.Namespace("notdefault");
private static final Id.Application appId = new Id.Application(namespace, AppWithWorkflow.NAME);
private static final Id.Program program = new Id.Program(appId, ProgramType.WORKFLOW,
AppWithWorkflow.SampleWorkflow.NAME);
private static final SchedulableProgramType programType = SchedulableProgramType.WORKFLOW;
private static final Id.Stream STREAM_ID = Id.Stream.from(namespace, "stream");
private static final Schedule TIME_SCHEDULE_0 = Schedules.builder("Schedule0")
.setDescription("Next 10 minutes")
.createTimeSchedule(getCron(10, TimeUnit.MINUTES));
private static final Schedule TIME_SCHEDULE_1 = Schedules.builder("Schedule1")
.setDescription("Next hour")
.createTimeSchedule(getCron(1, TimeUnit.HOURS));
private static final Schedule TIME_SCHEDULE_2 = Schedules.builder("Schedule2")
.setDescription("Next day")
.createTimeSchedule(getCron(1, TimeUnit.DAYS));
private static final Schedule TIME_SCHEDULE_3 = Schedules.builder("Schedule3")
.setDescription("Next Week")
.createTimeSchedule(getCron(7, TimeUnit.DAYS));
private static final Schedule DATA_SCHEDULE_1 = Schedules.builder("Schedule3")
.setDescription("Every 1M")
.createDataSchedule(Schedules.Source.STREAM, STREAM_ID.getId(), 1);
private static final Schedule DATA_SCHEDULE_2 = Schedules.builder("Schedule4")
.setDescription("Every 10M")
.createDataSchedule(Schedules.Source.STREAM, STREAM_ID.getId(), 10);
private static final Schedule UPDATED_TIME_SCHEDULE_1 = Schedules.builder("Schedule1")
.setDescription("Next 2 Hour")
.createTimeSchedule(getCron(2, TimeUnit.HOURS));
private static final Schedule UPDATED_DATA_SCHEDULE_2 = Schedules.builder("Schedule4")
.setDescription("Every 5M")
.createDataSchedule(Schedules.Source.STREAM, STREAM_ID.getId(), 5);
private ApplicationSpecification applicationSpecification;
@ClassRule
public static TemporaryFolder tmpFolder = new TemporaryFolder();
private static final Supplier<File> TEMP_FOLDER_SUPPLIER = new Supplier<File>() {
@Override
public File get() {
try {
return tmpFolder.newFolder();
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
};
@Rule
public final ExpectedException exception = ExpectedException.none();
@BeforeClass
public static void set() throws Exception {
schedulerService = AppFabricTestHelper.getInjector().getInstance(SchedulerService.class);
store = AppFabricTestHelper.getInjector().getInstance(Store.class);
locationFactory = AppFabricTestHelper.getInjector().getInstance(LocationFactory.class);
namespaceAdmin = AppFabricTestHelper.getInjector().getInstance(NamespaceAdmin.class);
namespaceAdmin.create(new NamespaceMeta.Builder().setName(namespace).build());
namespaceAdmin.create(NamespaceMeta.DEFAULT);
AppFabricTestHelper.deployApplicationWithManager(namespace, AppWithWorkflow.class, TEMP_FOLDER_SUPPLIER);
}
@AfterClass
public static void finish() throws Exception {
namespaceAdmin.delete(namespace);
namespaceAdmin.deleteDatasets(Id.Namespace.DEFAULT);
schedulerService.stopAndWait();
}
@Before
public void deployApp() throws Exception {
applicationSpecification = store.getApplication(appId);
}
@After
public void removeSchedules() throws SchedulerException {
schedulerService.deleteSchedules(program, programType);
applicationSpecification = deleteSchedulesFromSpec(applicationSpecification);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
}
@Test
public void testSchedulesAcrossNamespace() throws Exception {
schedulerService.schedule(program, programType, ImmutableList.of(TIME_SCHEDULE_1));
store.addApplication(appId, createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_1),
locationFactory.create("app"));
Id.Program programInOtherNamespace =
Id.Program.from(new Id.Application(new Id.Namespace("otherNamespace"), appId.getId()),
program.getType(), program.getId());
List<String> scheduleIds = schedulerService.getScheduleIds(program, programType);
Assert.assertEquals(1, scheduleIds.size());
List<String> scheduleIdsOtherNamespace = schedulerService.getScheduleIds(programInOtherNamespace, programType);
Assert.assertEquals(0, scheduleIdsOtherNamespace.size());
schedulerService.schedule(programInOtherNamespace, programType, ImmutableList.of(TIME_SCHEDULE_2));
store.addApplication(appId, createNewSpecification(applicationSpecification, programInOtherNamespace, programType,
TIME_SCHEDULE_2),
locationFactory.create("app"));
scheduleIdsOtherNamespace = schedulerService.getScheduleIds(programInOtherNamespace, programType);
Assert.assertEquals(1, scheduleIdsOtherNamespace.size());
Assert.assertNotEquals(scheduleIds.get(0), scheduleIdsOtherNamespace.get(0));
}
@Test
public void testSimpleSchedulerLifecycle() throws Exception {
schedulerService.schedule(program, programType, ImmutableList.of(TIME_SCHEDULE_1));
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_1);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
List<String> scheduleIds = schedulerService.getScheduleIds(program, programType);
Assert.assertEquals(1, scheduleIds.size());
checkState(Scheduler.ScheduleState.SUSPENDED, scheduleIds);
schedulerService.resumeSchedule(program, programType, "Schedule1");
checkState(Scheduler.ScheduleState.SCHEDULED, scheduleIds);
schedulerService.schedule(program, programType, TIME_SCHEDULE_2);
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_2);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
scheduleIds = schedulerService.getScheduleIds(program, programType);
Assert.assertEquals(2, scheduleIds.size());
schedulerService.resumeSchedule(program, programType, "Schedule2");
checkState(Scheduler.ScheduleState.SCHEDULED, scheduleIds);
schedulerService.schedule(program, programType, ImmutableList.of(DATA_SCHEDULE_1, DATA_SCHEDULE_2));
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, DATA_SCHEDULE_1);
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, DATA_SCHEDULE_2);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
scheduleIds = schedulerService.getScheduleIds(program, programType);
Assert.assertEquals(4, scheduleIds.size());
schedulerService.resumeSchedule(program, programType, "Schedule3");
schedulerService.resumeSchedule(program, programType, "Schedule4");
checkState(Scheduler.ScheduleState.SCHEDULED, scheduleIds);
schedulerService.suspendSchedule(program, SchedulableProgramType.WORKFLOW, "Schedule1");
schedulerService.suspendSchedule(program, SchedulableProgramType.WORKFLOW, "Schedule2");
checkState(Scheduler.ScheduleState.SUSPENDED, ImmutableList.of("Schedule1", "Schedule2"));
checkState(Scheduler.ScheduleState.SCHEDULED, ImmutableList.of("Schedule3", "Schedule4"));
schedulerService.suspendSchedule(program, SchedulableProgramType.WORKFLOW, "Schedule3");
schedulerService.suspendSchedule(program, SchedulableProgramType.WORKFLOW, "Schedule4");
checkState(Scheduler.ScheduleState.SUSPENDED, scheduleIds);
schedulerService.deleteSchedules(program, programType);
Assert.assertEquals(0, schedulerService.getScheduleIds(program, programType).size());
// Check the state of the old scheduleIds
// (which should be deleted by the call to SchedulerService#delete(Program, ProgramType)
checkState(Scheduler.ScheduleState.NOT_FOUND, scheduleIds);
}
@Test
public void testPausedTriggers() throws Exception {
schedulerService.schedule(program, programType, ImmutableList.of(TIME_SCHEDULE_1, TIME_SCHEDULE_2));
List<String> scheduleIds = schedulerService.getScheduleIds(program, programType);
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_1);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_2);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
Assert.assertEquals(2, scheduleIds.size());
// both the schedules should be in suspended state
checkState(Scheduler.ScheduleState.SUSPENDED, scheduleIds);
// schedule1 should go in scheduled state on resume
schedulerService.resumeSchedule(program, programType, "Schedule1");
checkState(Scheduler.ScheduleState.SCHEDULED, "Schedule1");
// schedule2 should still be in suspended state
checkState(Scheduler.ScheduleState.SUSPENDED, "Schedule2");
// add a new schedule and verify its in suspended state
schedulerService.schedule(program, programType, ImmutableList.of(TIME_SCHEDULE_0));
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, TIME_SCHEDULE_0);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
checkState(Scheduler.ScheduleState.SUSPENDED, "Schedule0");
// after adding a new schedule in paused state the resumed schedule should still be in resumed state
checkState(Scheduler.ScheduleState.SCHEDULED, "Schedule1");
// adding the schedule again which has been resumed and moved to default group should fail and throw an exception
testAddingResumedSchedule(ImmutableList.of(TIME_SCHEDULE_1));
// adding the schedule again which has been resumed and moved to default group should fail and throw an exception
// even if added with other new schedules
testAddingResumedSchedule(ImmutableList.of(TIME_SCHEDULE_3, TIME_SCHEDULE_1));
// TIME_SCHEDULE_3 should not have been added as it was being added with an existing schedule
checkState(Scheduler.ScheduleState.NOT_FOUND, "Schedule3");
}
private void testAddingResumedSchedule(ImmutableList<Schedule> scheduleList) {
try {
schedulerService.schedule(program, programType, scheduleList);
} catch (Exception e) {
Assert.assertTrue(e instanceof SchedulerException);
Assert.assertTrue(e.getCause() instanceof ObjectAlreadyExistsException);
}
}
@Test
public void testTimeScheduleUpdate() throws Exception {
testScheduleUpdate(TIME_SCHEDULE_1, UPDATED_TIME_SCHEDULE_1);
}
@Test
public void testDataScheduleUpdate() throws Exception {
testScheduleUpdate(DATA_SCHEDULE_2, UPDATED_DATA_SCHEDULE_2);
}
private void testScheduleUpdate(Schedule oldSchedule, Schedule newSchedule)
throws Exception {
schedulerService.schedule(program, programType, ImmutableList.of(oldSchedule));
applicationSpecification = createNewSpecification(applicationSpecification, program, programType, oldSchedule);
store.addApplication(appId, applicationSpecification, locationFactory.create("app"));
List<String> scheduleIds = schedulerService.getScheduleIds(program, programType);
// schedule should be deployed in suspended state
Assert.assertEquals(1, scheduleIds.size());
checkState(Scheduler.ScheduleState.SUSPENDED, scheduleIds);
List<ScheduledRuntime> oldScheduledRuntimes = schedulerService.nextScheduledRuntime(program, programType);
// schedule is paused and thus the nextRuntime should be empty
Assert.assertTrue(oldScheduledRuntimes.isEmpty());
schedulerService.resumeSchedule(program, programType, oldSchedule.getName());
oldScheduledRuntimes = schedulerService.nextScheduledRuntime(program, programType);
schedulerService.suspendSchedule(program, programType, oldSchedule.getName());
// update a newly created schedule which is in suspended state
schedulerService.updateSchedule(program, programType, newSchedule);
scheduleIds = schedulerService.getScheduleIds(program, programType);
// there should be only one schedule for this program
Assert.assertEquals(1, scheduleIds.size());
// schedules should still be in suspended state after update
checkState(Scheduler.ScheduleState.SUSPENDED, scheduleIds);
// time schedules will have nextRuntime associated with it so verify that they are correct after update
if (oldSchedule instanceof TimeSchedule && newSchedule instanceof TimeSchedule) {
// resume schedule before verifying next runtime
schedulerService.resumeSchedule(program, programType, newSchedule.getName());
verifyUpdatedNextRuntime(oldScheduledRuntimes);
schedulerService.suspendSchedule(program, programType, newSchedule.getName());
}
// the state of an resumed schedule should remain resumed even after update
schedulerService.resumeSchedule(program, programType, newSchedule.getName());
schedulerService.updateSchedule(program, programType, oldSchedule);
scheduleIds = schedulerService.getScheduleIds(program, programType);
checkState(Scheduler.ScheduleState.SCHEDULED, scheduleIds);
}
private void verifyUpdatedNextRuntime(List<ScheduledRuntime> oldScheduledRuntimes)
throws SchedulerException {
List<ScheduledRuntime> updatedScheduledRuntimes = schedulerService.nextScheduledRuntime(program, programType);
// the updated next schedule runtime must be greater than the old one
Assert.assertTrue(updatedScheduledRuntimes.get(0).getTime() > oldScheduledRuntimes.get(0).getTime());
}
/**
* Returns a crontab that will get triggered after {@code offset} time in the given unit from current time.
*/
private static String getCron(long offset, TimeUnit unit) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis() + unit.toMillis(offset));
return String.format("%s %s %s %s *",
calendar.get(Calendar.MINUTE), calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.DAY_OF_MONTH), calendar.get(Calendar.MONTH) + 1);
}
private void checkState(Scheduler.ScheduleState expectedState, List<String> scheduleIds) throws Exception {
for (String scheduleId : scheduleIds) {
checkState(expectedState, scheduleId);
}
}
private void checkState(Scheduler.ScheduleState expectedState, String scheduleId) throws SchedulerException {
int i = scheduleId.lastIndexOf(':');
Assert.assertEquals(expectedState, schedulerService.scheduleState(program, SchedulableProgramType.WORKFLOW,
scheduleId.substring(i + 1)));
}
private ApplicationSpecification createNewSpecification(ApplicationSpecification spec, Id.Program programId,
SchedulableProgramType programType, Schedule schedule) {
ImmutableMap.Builder<String, ScheduleSpecification> builder = ImmutableMap.builder();
builder.putAll(spec.getSchedules());
builder.put(schedule.getName(), new ScheduleSpecification(schedule,
new ScheduleProgramInfo(programType, programId.getId()),
ImmutableMap.<String, String>of()));
return new DefaultApplicationSpecification(
spec.getName(),
spec.getDescription(),
spec.getConfiguration(),
spec.getArtifactId(),
spec.getStreams(),
spec.getDatasetModules(),
spec.getDatasets(),
spec.getFlows(),
spec.getMapReduce(),
spec.getSpark(),
spec.getWorkflows(),
spec.getServices(),
builder.build(),
spec.getWorkers(),
spec.getPlugins()
);
}
private ApplicationSpecification deleteSchedulesFromSpec(ApplicationSpecification spec) {
return new DefaultApplicationSpecification(
spec.getName(),
spec.getDescription(),
spec.getConfiguration(),
spec.getArtifactId(),
spec.getStreams(),
spec.getDatasetModules(),
spec.getDatasets(),
spec.getFlows(),
spec.getMapReduce(),
spec.getSpark(),
spec.getWorkflows(),
spec.getServices(),
ImmutableMap.<String, ScheduleSpecification>of(),
spec.getWorkers(),
spec.getPlugins()
);
}
}