/** * 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.concurrent.CountDownLatch; import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.util.Modules; import org.apache.aurora.common.stats.StatsProvider; import org.apache.aurora.common.testing.easymock.EasyMockTest; import org.apache.aurora.common.util.Clock; import org.apache.aurora.gen.Container; import org.apache.aurora.gen.Identity; import org.apache.aurora.gen.JobConfiguration; import org.apache.aurora.gen.MesosContainer; import org.apache.aurora.gen.TaskConfig; import org.apache.aurora.scheduler.base.JobKeys; import org.apache.aurora.scheduler.base.TaskTestUtil; import org.apache.aurora.scheduler.configuration.ConfigurationManager; import org.apache.aurora.scheduler.cron.CronJobManager; import org.apache.aurora.scheduler.cron.CrontabEntry; import org.apache.aurora.scheduler.cron.SanitizedCronJob; import org.apache.aurora.scheduler.events.EventSink; 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.IJobConfiguration; import org.apache.aurora.scheduler.storage.entities.IJobKey; import org.junit.Before; import org.junit.Test; import org.quartz.JobExecutionContext; import org.quartz.Scheduler; import org.quartz.Trigger; import org.quartz.TriggerListener; import static org.apache.aurora.gen.CronCollisionPolicy.KILL_EXISTING; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class CronIT extends EasyMockTest { public static final CrontabEntry CRONTAB_ENTRY = CrontabEntry.parse("* * * * *"); private static final IJobKey JOB_KEY = JobKeys.from("roll", "b", "c"); private static final Identity IDENTITY = new Identity().setUser("user"); private static final IJobConfiguration CRON_JOB = IJobConfiguration.build( new JobConfiguration() .setCronSchedule(CRONTAB_ENTRY.toString()) .setKey(JOB_KEY.newBuilder()) .setInstanceCount(2) .setOwner(IDENTITY) .setCronCollisionPolicy(KILL_EXISTING) .setTaskConfig(makeTaskConfig())); private Injector injector; private StateManager stateManager; private Storage storage; private AuroraCronJob auroraCronJob; @Before public void setUp() throws Exception { stateManager = createMock(StateManager.class); storage = DbUtil.createStorage(); auroraCronJob = createMock(AuroraCronJob.class); injector = Guice.createInjector( // Override to verify that Guice is actually used for construction of the AuroraCronJob. // TODO(ksweeney): Use the production class here. Modules.override(new CronModule()).with(new AbstractModule() { @Override protected void configure() { bind(AuroraCronJob.class).toInstance(auroraCronJob); } }), new AbstractModule() { @Override protected void configure() { bind(ConfigurationManager.class).toInstance(TaskTestUtil.CONFIGURATION_MANAGER); bind(Clock.class).toInstance(Clock.SYSTEM_CLOCK); bind(StateManager.class).toInstance(stateManager); bind(Storage.class).toInstance(storage); bind(StatsProvider.class).toInstance(createMock(StatsProvider.class)); bind(EventSink.class).toInstance(createMock(EventSink.class)); } }); } private static TaskConfig makeTaskConfig() { TaskConfig config = TaskTestUtil.makeConfig(JOB_KEY).newBuilder(); config.setIsService(false); // Bypassing a command-line argument in ConfigurationManager that by default disallows the // docker container type. config.setContainer(Container.mesos(new MesosContainer())); return config; } private Service boot() { Service service = injector.getInstance(CronLifecycle.class); service.startAsync().awaitRunning(); return service; } @Test public void testCronSchedulerLifecycle() throws Exception { control.replay(); Scheduler scheduler = injector.getInstance(Scheduler.class); assertFalse(scheduler.isStarted()); Service cronLifecycle = boot(); assertTrue(cronLifecycle.isRunning()); assertTrue(scheduler.isStarted()); cronLifecycle.stopAsync().awaitTerminated(); assertFalse(cronLifecycle.isRunning()); assertTrue(scheduler.isShutdown()); } @Test public void testJobsAreScheduled() throws Exception { auroraCronJob.execute(isA(JobExecutionContext.class)); control.replay(); final Scheduler scheduler = injector.getInstance(Scheduler.class); storage.write((NoResult.Quiet) storeProvider -> storeProvider.getCronJobStore().saveAcceptedJob(CRON_JOB)); final CountDownLatch cronRan = new CountDownLatch(1); scheduler.getListenerManager().addTriggerListener(new CountDownWhenComplete(cronRan)); Service service = boot(); cronRan.await(); service.stopAsync().awaitTerminated(); } @Test public void testKillExistingDogpiles() throws Exception { // Test that a trigger for a job that hasn't finished running is ignored. final CronJobManager cronJobManager = injector.getInstance(CronJobManager.class); final CountDownLatch firstExecutionTriggered = new CountDownLatch(1); final CountDownLatch firstExecutionCompleted = new CountDownLatch(1); final CountDownLatch secondExecutionTriggered = new CountDownLatch(1); final CountDownLatch secondExecutionCompleted = new CountDownLatch(1); auroraCronJob.execute(isA(JobExecutionContext.class)); expectLastCall().andAnswer(() -> { firstExecutionTriggered.countDown(); firstExecutionCompleted.await(); return null; }); auroraCronJob.execute(isA(JobExecutionContext.class)); expectLastCall().andAnswer(() -> { secondExecutionTriggered.countDown(); secondExecutionCompleted.await(); return null; }); control.replay(); boot(); cronJobManager.createJob(SanitizedCronJob.fromUnsanitized( TaskTestUtil.CONFIGURATION_MANAGER, CRON_JOB)); cronJobManager.startJobNow(JOB_KEY); firstExecutionTriggered.await(); cronJobManager.startJobNow(JOB_KEY); assertEquals(1, secondExecutionTriggered.getCount()); firstExecutionCompleted.countDown(); secondExecutionTriggered.await(); secondExecutionTriggered.countDown(); } private static class CountDownWhenComplete implements TriggerListener { private final CountDownLatch countDownLatch; CountDownWhenComplete(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } @Override public String getName() { return CountDownWhenComplete.class.getName(); } @Override public void triggerFired(Trigger trigger, JobExecutionContext context) { // No-op. } @Override public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { return false; } @Override public void triggerMisfired(Trigger trigger) { // No-op. } @Override public void triggerComplete( Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) { countDownLatch.countDown(); // No-op. } } }