/*
* Copyright 2001-2009 Terracotta, 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 org.quartz;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.JobKey.jobKey;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.TriggerKey.triggerKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.quartz.Trigger.TriggerState;
import org.quartz.impl.matchers.GroupMatcher;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Test High Level Scheduler functionality (implicitly tests the underlying jobstore (RAMJobStore))
*/
public abstract class AbstractSchedulerTest {
private static final String BARRIER = "BARRIER";
private static final String DATE_STAMPS = "DATE_STAMPS";
private static final String JOB_THREAD = "JOB_THREAD";
@SuppressWarnings("deprecation")
public static class TestStatefulJob implements StatefulJob {
public void execute(JobExecutionContext context)
throws JobExecutionException {
}
}
public static class TestJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
}
}
public static final long TEST_TIMEOUT_SECONDS = 125;
public static class TestJobWithSync implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
try {
@SuppressWarnings("unchecked")
List<Long> jobExecTimestamps = (List<Long>)context.getScheduler().getContext().get(DATE_STAMPS);
CyclicBarrier barrier = (CyclicBarrier)context.getScheduler().getContext().get(BARRIER);
jobExecTimestamps.add(System.currentTimeMillis());
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Throwable e) {
e.printStackTrace();
throw new AssertionError("Await on barrier was interrupted: " + e.toString());
}
}
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class TestAnnotatedJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
}
}
protected abstract Scheduler createScheduler(String name, int threadPoolSize) throws SchedulerException;
@Test
public void testBasicStorageFunctions() throws Exception {
Scheduler sched = createScheduler("testBasicStorageFunctions", 2);
// test basic storage functions of scheduler...
JobDetail job = newJob()
.ofType(TestJob.class)
.withIdentity("j1")
.storeDurably()
.build();
assertFalse("Unexpected existence of job named 'j1'.", sched.checkExists(jobKey("j1")));
sched.addJob(job, false);
assertTrue("Expected existence of job named 'j1' but checkExists return false.", sched.checkExists(jobKey("j1")));
job = sched.getJobDetail(jobKey("j1"));
assertNotNull("Stored job not found!", job);
sched.deleteJob(jobKey("j1"));
Trigger trigger = newTrigger()
.withIdentity("t1")
.forJob(job)
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInSeconds(5))
.build();
assertFalse("Unexpected existence of trigger named '11'.", sched.checkExists(triggerKey("t1")));
sched.scheduleJob(job, trigger);
assertTrue("Expected existence of trigger named 't1' but checkExists return false.", sched.checkExists(triggerKey("t1")));
job = sched.getJobDetail(jobKey("j1"));
assertNotNull("Stored job not found!", job);
trigger = sched.getTrigger(triggerKey("t1"));
assertNotNull("Stored trigger not found!", trigger);
job = newJob()
.ofType(TestJob.class)
.withIdentity("j2", "g1")
.build();
trigger = newTrigger()
.withIdentity("t2", "g1")
.forJob(job)
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInSeconds(5))
.build();
sched.scheduleJob(job, trigger);
job = newJob()
.ofType(TestJob.class)
.withIdentity("j3", "g1")
.build();
trigger = newTrigger()
.withIdentity("t3", "g1")
.forJob(job)
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInSeconds(5))
.build();
sched.scheduleJob(job, trigger);
List<String> jobGroups = sched.getJobGroupNames();
List<String> triggerGroups = sched.getTriggerGroupNames();
assertTrue("Job group list size expected to be = 2 ", jobGroups.size() == 2);
assertTrue("Trigger group list size expected to be = 2 ", triggerGroups.size() == 2);
Set<JobKey> jobKeys = sched.getJobKeys(GroupMatcher.jobGroupEquals(JobKey.DEFAULT_GROUP));
Set<TriggerKey> triggerKeys = sched.getTriggerKeys(GroupMatcher.triggerGroupEquals(TriggerKey.DEFAULT_GROUP));
assertTrue("Number of jobs expected in default group was 1 ", jobKeys.size() == 1);
assertTrue("Number of triggers expected in default group was 1 ", triggerKeys.size() == 1);
jobKeys = sched.getJobKeys(GroupMatcher.jobGroupEquals("g1"));
triggerKeys = sched.getTriggerKeys(GroupMatcher.triggerGroupEquals("g1"));
assertTrue("Number of jobs expected in 'g1' group was 2 ", jobKeys.size() == 2);
assertTrue("Number of triggers expected in 'g1' group was 2 ", triggerKeys.size() == 2);
TriggerState s = sched.getTriggerState(triggerKey("t2", "g1"));
assertTrue("State of trigger t2 expected to be NORMAL ", s.equals(TriggerState.NORMAL));
sched.pauseTrigger(triggerKey("t2", "g1"));
s = sched.getTriggerState(triggerKey("t2", "g1"));
assertTrue("State of trigger t2 expected to be PAUSED ", s.equals(TriggerState.PAUSED));
sched.resumeTrigger(triggerKey("t2", "g1"));
s = sched.getTriggerState(triggerKey("t2", "g1"));
assertTrue("State of trigger t2 expected to be NORMAL ", s.equals(TriggerState.NORMAL));
Set<String> pausedGroups = sched.getPausedTriggerGroups();
assertTrue("Size of paused trigger groups list expected to be 0 ", pausedGroups.size() == 0);
sched.pauseTriggers(GroupMatcher.triggerGroupEquals("g1"));
// test that adding a trigger to a paused group causes the new trigger to be paused also...
job = newJob()
.ofType(TestJob.class)
.withIdentity("j4", "g1")
.build();
trigger = newTrigger()
.withIdentity("t4", "g1")
.forJob(job)
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInSeconds(5))
.build();
sched.scheduleJob(job, trigger);
pausedGroups = sched.getPausedTriggerGroups();
assertTrue("Size of paused trigger groups list expected to be 1 ", pausedGroups.size() == 1);
s = sched.getTriggerState(triggerKey("t2", "g1"));
assertTrue("State of trigger t2 expected to be PAUSED ", s.equals(TriggerState.PAUSED));
s = sched.getTriggerState(triggerKey("t4", "g1"));
assertTrue("State of trigger t4 expected to be PAUSED ", s.equals(TriggerState.PAUSED));
sched.resumeTriggers(GroupMatcher.triggerGroupEquals("g1"));
s = sched.getTriggerState(triggerKey("t2", "g1"));
assertTrue("State of trigger t2 expected to be NORMAL ", s.equals(TriggerState.NORMAL));
s = sched.getTriggerState(triggerKey("t4", "g1"));
assertTrue("State of trigger t4 expected to be NORMAL ", s.equals(TriggerState.NORMAL));
pausedGroups = sched.getPausedTriggerGroups();
assertTrue("Size of paused trigger groups list expected to be 0 ", pausedGroups.size() == 0);
assertFalse("Scheduler should have returned 'false' from attempt to unschedule non-existing trigger. ", sched.unscheduleJob(triggerKey("foasldfksajdflk")));
assertTrue("Scheduler should have returned 'true' from attempt to unschedule existing trigger. ", sched.unscheduleJob(triggerKey("t3", "g1")));
jobKeys = sched.getJobKeys(GroupMatcher.jobGroupEquals("g1"));
triggerKeys = sched.getTriggerKeys(GroupMatcher.triggerGroupEquals("g1"));
assertTrue("Number of jobs expected in 'g1' group was 1 ", jobKeys.size() == 2); // job should have been deleted also, because it is non-durable
assertTrue("Number of triggers expected in 'g1' group was 1 ", triggerKeys.size() == 2);
assertTrue("Scheduler should have returned 'true' from attempt to unschedule existing trigger. ", sched.unscheduleJob(triggerKey("t1")));
jobKeys = sched.getJobKeys(GroupMatcher.jobGroupEquals(JobKey.DEFAULT_GROUP));
triggerKeys = sched.getTriggerKeys(GroupMatcher.triggerGroupEquals(TriggerKey.DEFAULT_GROUP));
assertTrue("Number of jobs expected in default group was 1 ", jobKeys.size() == 1); // job should have been left in place, because it is non-durable
assertTrue("Number of triggers expected in default group was 0 ", triggerKeys.size() == 0);
sched.shutdown(true);
}
@Test
public void testDurableStorageFunctions() throws Exception {
Scheduler sched = createScheduler("testDurableStorageFunctions", 2);
try {
// test basic storage functions of scheduler...
JobDetail job = newJob()
.ofType(TestJob.class)
.withIdentity("j1")
.storeDurably()
.build();
assertFalse("Unexpected existence of job named 'j1'.", sched.checkExists(jobKey("j1")));
sched.addJob(job, false);
assertTrue("Unexpected non-existence of job named 'j1'.", sched.checkExists(jobKey("j1")));
JobDetail nonDurableJob = newJob()
.ofType(TestJob.class)
.withIdentity("j2")
.build();
try {
sched.addJob(nonDurableJob, false);
fail("Storage of non-durable job should not have succeeded.");
}
catch(SchedulerException expected) {
assertFalse("Unexpected existence of job named 'j2'.", sched.checkExists(jobKey("j2")));
}
sched.addJob(nonDurableJob, false, true);
assertTrue("Unexpected non-existence of job named 'j2'.", sched.checkExists(jobKey("j2")));
} finally {
sched.shutdown(true);
}
}
@Test
public void testShutdownWithSleepReturnsAfterAllThreadsAreStopped() throws Exception {
Map<Thread, StackTraceElement[]> allThreadsStart = Thread.getAllStackTraces();
int threadPoolSize = 5;
Scheduler scheduler = createScheduler("testShutdownWithSleepReturnsAfterAllThreadsAreStopped", threadPoolSize);
Thread.sleep(500L);
Map<Thread, StackTraceElement[]> allThreadsRunning = Thread.getAllStackTraces();
scheduler.shutdown( true );
Thread.sleep(200L);
Map<Thread, StackTraceElement[]> allThreadsEnd = Thread.getAllStackTraces();
Set<Thread> endingThreads = new HashSet<Thread>(allThreadsEnd.keySet());
// remove all pre-existing threads from the set
for(Thread t: allThreadsStart.keySet()) {
allThreadsEnd.remove(t);
}
// remove threads that are known artifacts of the test
for(Thread t: endingThreads) {
if(t.getName().contains("derby") && t.getThreadGroup().getName().contains("derby")) {
allThreadsEnd.remove(t);
}
if(t.getThreadGroup() != null && t.getThreadGroup().getName().equals("system")) {
allThreadsEnd.remove(t);
}
if(t.getThreadGroup() != null && t.getThreadGroup().getName().equals("main")) {
allThreadsEnd.remove(t);
}
}
if(allThreadsEnd.size() > 0) {
// log the additional threads
for(Thread t: allThreadsEnd.keySet()) {
System.out.println("*** Found additional thread: " + t.getName() + " (of type " + t.getClass().getName() +") in group: " + t.getThreadGroup().getName() + " with parent group: " + (t.getThreadGroup().getParent() == null ? "-none-" : t.getThreadGroup().getParent().getName()));
}
// log all threads that were running before shutdown
for(Thread t: allThreadsRunning.keySet()) {
System.out.println("- Test runtime thread: " + t.getName() + " (of type " + t.getClass().getName() +") in group: " + (t.getThreadGroup() == null ? "-none-" : (t.getThreadGroup().getName() + " with parent group: " + (t.getThreadGroup().getParent() == null ? "-none-" : t.getThreadGroup().getParent().getName()))));
}
}
assertTrue( "Found unexpected new threads (see console output for listing)", allThreadsEnd.size() == 0 );
}
@Test
public void testAbilityToFireImmediatelyWhenStartedBefore() throws Exception {
List<Long> jobExecTimestamps = Collections.synchronizedList(new ArrayList<Long>());
CyclicBarrier barrier = new CyclicBarrier(2);
Scheduler sched = createScheduler("testAbilityToFireImmediatelyWhenStartedBefore", 5);
sched.getContext().put(BARRIER, barrier);
sched.getContext().put(DATE_STAMPS, jobExecTimestamps);
sched.start();
Thread.yield();
JobDetail job1 = JobBuilder.newJob(TestJobWithSync.class).withIdentity("job1").build();
Trigger trigger1 = TriggerBuilder.newTrigger().forJob(job1).build();
long sTime = System.currentTimeMillis();
sched.scheduleJob(job1, trigger1);
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
sched.shutdown(true);
long fTime = jobExecTimestamps.get(0);
assertTrue("Immediate trigger did not fire within a reasonable amount of time.", (fTime - sTime < 7000L)); // This is dangerously subjective! but what else to do?
}
@Test
public void testAbilityToFireImmediatelyWhenStartedBeforeWithTriggerJob() throws Exception {
List<Long> jobExecTimestamps = Collections.synchronizedList(new ArrayList<Long>());
CyclicBarrier barrier = new CyclicBarrier(2);
Scheduler sched = createScheduler("testAbilityToFireImmediatelyWhenStartedBeforeWithTriggerJob", 5);
sched.getContext().put(BARRIER, barrier);
sched.getContext().put(DATE_STAMPS, jobExecTimestamps);
sched.start();
Thread.yield();
JobDetail job1 = JobBuilder.newJob(TestJobWithSync.class).withIdentity("job1").storeDurably().build();
sched.addJob(job1, false);
long sTime = System.currentTimeMillis();
sched.triggerJob(job1.getKey());
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
sched.shutdown(true);
long fTime = jobExecTimestamps.get(0);
assertTrue("Immediate trigger did not fire within a reasonable amount of time.", (fTime - sTime < 7000L)); // This is dangerously subjective! but what else to do?
}
@Test
public void testAbilityToFireImmediatelyWhenStartedAfter() throws Exception {
List<Long> jobExecTimestamps = Collections.synchronizedList(new ArrayList<Long>());
CyclicBarrier barrier = new CyclicBarrier(2);
Scheduler sched = createScheduler("testAbilityToFireImmediatelyWhenStartedAfter", 5);
sched.getContext().put(BARRIER, barrier);
sched.getContext().put(DATE_STAMPS, jobExecTimestamps);
JobDetail job1 = JobBuilder.newJob(TestJobWithSync.class).withIdentity("job1").build();
Trigger trigger1 = TriggerBuilder.newTrigger().forJob(job1).build();
long sTime = System.currentTimeMillis();
sched.scheduleJob(job1, trigger1);
sched.start();
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
sched.shutdown(true);
long fTime = jobExecTimestamps.get(0);
assertTrue("Immediate trigger did not fire within a reasonable amount of time.", (fTime - sTime < 7000L)); // This is dangerously subjective! but what else to do?
}
@Test
public void testScheduleMultipleTriggersForAJob() throws SchedulerException {
JobDetail job = newJob(TestJob.class).withIdentity("job1", "group1").build();
Trigger trigger1 = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
.repeatForever())
.build();
Trigger trigger2 = newTrigger()
.withIdentity("trigger2", "group1")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
.repeatForever())
.build();
Set<Trigger> triggersForJob = new HashSet<Trigger>();
triggersForJob.add(trigger1);
triggersForJob.add(trigger2);
Scheduler sched = createScheduler("testScheduleMultipleTriggersForAJob", 5);
sched.scheduleJob(job,triggersForJob, true);
List<? extends Trigger> triggersOfJob = sched.getTriggersOfJob(job.getKey());
assertEquals(2,triggersOfJob.size());
assertTrue(triggersOfJob.contains(trigger1));
assertTrue(triggersOfJob.contains(trigger2));
sched.shutdown(true);
}
@Test
public void testShutdownWithoutWaitIsUnclean() throws Exception {
CyclicBarrier barrier = new CyclicBarrier(2);
Scheduler scheduler = createScheduler("testShutdownWithoutWaitIsUnclean", 8);
try {
scheduler.getContext().put(BARRIER, barrier);
scheduler.start();
scheduler.addJob(newJob().ofType(UncleanShutdownJob.class).withIdentity("job").storeDurably().build(), false);
scheduler.scheduleJob(newTrigger().forJob("job").startNow().build());
while (scheduler.getCurrentlyExecutingJobs().isEmpty()) {
Thread.sleep(50);
}
} finally {
scheduler.shutdown(false);
}
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
Thread jobThread = (Thread) scheduler.getContext().get(JOB_THREAD);
jobThread.join(TimeUnit.SECONDS.toMillis(TEST_TIMEOUT_SECONDS));
}
public static class UncleanShutdownJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
SchedulerContext schedulerContext = context.getScheduler().getContext();
schedulerContext.put(JOB_THREAD, Thread.currentThread());
CyclicBarrier barrier = (CyclicBarrier) schedulerContext.get(BARRIER);
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Throwable e) {
e.printStackTrace();
throw new AssertionError("Await on barrier was interrupted: " + e.toString());
}
}
}
@Test
public void testShutdownWithWaitIsClean() throws Exception {
final AtomicBoolean shutdown = new AtomicBoolean(false);
List<Long> jobExecTimestamps = Collections.synchronizedList(new ArrayList<Long>());
CyclicBarrier barrier = new CyclicBarrier(2);
final Scheduler scheduler = createScheduler("testShutdownWithWaitIsClean", 8);
try {
scheduler.getContext().put(BARRIER, barrier);
scheduler.getContext().put(DATE_STAMPS, jobExecTimestamps);
scheduler.start();
scheduler.addJob(newJob().ofType(TestJobWithSync.class).withIdentity("job").storeDurably().build(), false);
scheduler.scheduleJob(newTrigger().forJob("job").startNow().build());
while (scheduler.getCurrentlyExecutingJobs().isEmpty()) {
Thread.sleep(50);
}
} finally {
Thread t = new Thread() {
@Override
public void run() {
try {
scheduler.shutdown(true);
shutdown.set(true);
} catch (SchedulerException ex) {
throw new RuntimeException(ex);
}
}
};
t.start();
Thread.sleep(1000);
assertFalse(shutdown.get());
barrier.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
t.join();
}
}
}