/*
* Copyright © 2015 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.AppWithStreamSizeSchedule;
import co.cask.cdap.api.metrics.MetricStore;
import co.cask.cdap.api.schedule.SchedulableProgramType;
import co.cask.cdap.api.schedule.Schedule;
import co.cask.cdap.api.schedule.Schedules;
import co.cask.cdap.app.runtime.ProgramRuntimeService;
import co.cask.cdap.app.store.Store;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.namespace.NamespaceAdmin;
import co.cask.cdap.common.utils.Tasks;
import co.cask.cdap.internal.AppFabricTestHelper;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.proto.ProgramRunStatus;
import co.cask.cdap.proto.ProgramType;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.inject.Injector;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* Base class for scheduler tests
*/
public abstract class SchedulerTestBase {
protected static final CConfiguration CCONF = CConfiguration.create();
private static StreamSizeScheduler streamSizeScheduler;
private static Store store;
private static NamespaceAdmin namespaceAdmin;
private static ProgramRuntimeService runtimeService;
protected static MetricStore metricStore;
protected static Injector injector;
private static final Id.Application APP_ID = new Id.Application(Id.Namespace.DEFAULT,
"AppWithStreamSizeSchedule");
private static final Id.Program PROGRAM_ID = new Id.Program(APP_ID, ProgramType.WORKFLOW, "SampleWorkflow");
private static final String SCHEDULE_NAME_1 = "SampleSchedule1";
private static final String SCHEDULE_NAME_2 = "SampleSchedule2";
private static final SchedulableProgramType PROGRAM_TYPE = SchedulableProgramType.WORKFLOW;
private static final Id.Stream STREAM_ID = Id.Stream.from(Id.Namespace.DEFAULT, "stream");
private static final Schedule UPDATE_SCHEDULE_2 = Schedules.builder(SCHEDULE_NAME_2)
.setDescription("Every 1M")
.createDataSchedule(Schedules.Source.STREAM, STREAM_ID.getId(), 1);
@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);
}
}
};
protected interface StreamMetricsPublisher {
void increment(long size) throws Exception;
}
protected abstract StreamMetricsPublisher createMetricsPublisher(Id.Stream streamId);
@BeforeClass
public static void init() throws Exception {
injector = AppFabricTestHelper.getInjector(CCONF);
streamSizeScheduler = injector.getInstance(StreamSizeScheduler.class);
store = injector.getInstance(Store.class);
metricStore = injector.getInstance(MetricStore.class);
namespaceAdmin = injector.getInstance(NamespaceAdmin.class);
namespaceAdmin.create(NamespaceMeta.DEFAULT);
runtimeService = injector.getInstance(ProgramRuntimeService.class);
}
@Test
public void testStreamSizeSchedule() throws Exception {
// Test the StreamSizeScheduler behavior using notifications
AppFabricTestHelper.deployApplicationWithManager(AppWithStreamSizeSchedule.class, TEMP_FOLDER_SUPPLIER);
Assert.assertEquals(Scheduler.ScheduleState.SUSPENDED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_1));
Assert.assertEquals(Scheduler.ScheduleState.SUSPENDED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2));
streamSizeScheduler.resumeSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_1);
streamSizeScheduler.resumeSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
Assert.assertEquals(Scheduler.ScheduleState.SCHEDULED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_1));
Assert.assertEquals(Scheduler.ScheduleState.SCHEDULED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2));
int runs = store.getRuns(PROGRAM_ID, ProgramRunStatus.ALL, 0, Long.MAX_VALUE, 100).size();
Assert.assertEquals(0, runs);
StreamMetricsPublisher metricsPublisher = createMetricsPublisher(STREAM_ID);
// Publish a notification on behalf of the stream with enough data to trigger the execution of the job
metricsPublisher.increment(1024 * 1024);
waitForRuns(store, PROGRAM_ID, 1, 15);
waitUntilFinished(runtimeService, PROGRAM_ID, 15);
// Trigger both scheduled program
metricsPublisher.increment(1024 * 1024);
waitForRuns(store, PROGRAM_ID, 3, 15);
// Suspend a schedule multiple times, and make sur that it doesn't mess up anything
streamSizeScheduler.suspendSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
streamSizeScheduler.suspendSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
Assert.assertEquals(Scheduler.ScheduleState.SUSPENDED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2));
// Since schedule 2 is suspended, only the first schedule should get triggered
metricsPublisher.increment(1024 * 1024);
waitForRuns(store, PROGRAM_ID, 4, 15);
// Resume schedule 2
streamSizeScheduler.resumeSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
streamSizeScheduler.resumeSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
Assert.assertEquals(Scheduler.ScheduleState.SCHEDULED,
streamSizeScheduler.scheduleState(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2));
// Both schedules should be trigger. In particular, the schedule that has just been resumed twice should
// only trigger once
metricsPublisher.increment(1024 * 1024);
waitForRuns(store, PROGRAM_ID, 6, 15);
// Update the schedule2's data trigger
// Both schedules should now trigger execution after 1 MB of data received
streamSizeScheduler.updateSchedule(PROGRAM_ID, PROGRAM_TYPE, UPDATE_SCHEDULE_2);
metricsPublisher.increment(1024 * 1024);
waitForRuns(store, PROGRAM_ID, 8, 15);
streamSizeScheduler.suspendSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_1);
streamSizeScheduler.suspendSchedule(PROGRAM_ID, PROGRAM_TYPE, SCHEDULE_NAME_2);
streamSizeScheduler.deleteSchedules(PROGRAM_ID, PROGRAM_TYPE);
waitUntilFinished(runtimeService, PROGRAM_ID, 10);
}
@AfterClass
public static void tearDown() throws Exception {
namespaceAdmin.delete(Id.Namespace.DEFAULT);
}
/**
* Waits until there is nothing running for the given program.
*/
private void waitUntilFinished(final ProgramRuntimeService runtimeService,
final Id.Program program, long maxWaitSeconds) throws Exception {
Tasks.waitFor(false, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return isProgramRunning(runtimeService, program);
}
}, maxWaitSeconds, TimeUnit.SECONDS, 50, TimeUnit.MILLISECONDS);
}
/**
* Waits for the given program ran for the given number of times.
*/
private void waitForRuns(final Store store, final Id.Program programId,
int expectedRuns, long timeoutSeconds) throws Exception {
Tasks.waitFor(expectedRuns, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return store.getRuns(programId, ProgramRunStatus.COMPLETED, 0, Long.MAX_VALUE, 100).size();
}
}, timeoutSeconds, TimeUnit.SECONDS, 50, TimeUnit.MILLISECONDS);
}
private boolean isProgramRunning(ProgramRuntimeService runtimeService, final Id.Program program) {
return runtimeService.checkAnyRunning(new Predicate<Id.Program>() {
@Override
public boolean apply(Id.Program programId) {
return programId.equals(program);
}
}, ProgramType.values());
}
}