Adds QuartzScheduleServiceProvider.

This commit is contained in:
Marius Bogoevici
2008-08-19 23:18:58 +00:00
parent 42e7a6bffa
commit e0a760fff6
6 changed files with 537 additions and 30 deletions

View File

@@ -1,24 +0,0 @@
/*
* Copyright 2002-2008 the original author or authors.
*
* 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.springframework.integration.quartz;
/**
* @author Mark Fisher
*/
public class Placeholder {
}

View File

@@ -0,0 +1,306 @@
/*
* Copyright 2002-2008 the original author or authors.
*
* 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.springframework.integration.quartz;
import java.util.Date;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.quartz.CronTrigger;
import org.quartz.InterruptableJob;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.UnableToInterruptJobException;
import org.quartz.listeners.JobListenerSupport;
import org.springframework.integration.scheduling.spi.ScheduleServiceProvider;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
/**
* A Quartz-based implementation of the {@link org.springframework.integration.scheduling.spi.ScheduleServiceProvider}.
*
* @author Marius Bogoevici
*/
public class QuartzScheduleServiceProvider implements ScheduleServiceProvider {
private static final String RUN_METHOD_NAME = "run";
private static final String RUNNABLE_INSTANCE = "runnable.instance";
private static final String FIXED_DELAY_PARAMETER = "fixedDelay";
private static final String TIME_UNIT_PARAMETER = "timeUnit";
private static final String TRIGGER_NAME_PARAMETER = "triggerName";
private final Scheduler scheduler;
private final JobListener fixedDelayJobListener;
public QuartzScheduleServiceProvider(Scheduler scheduler) {
this.scheduler = scheduler;
this.fixedDelayJobListener = new FixedDelayJobListener();
try {
this.scheduler.addJobListener(this.fixedDelayJobListener);
}
catch (SchedulerException e) {
throw new QuartzSchedulingException(e);
}
}
public void execute(Runnable runnable) {
try {
scheduler.scheduleJob(wrapAsJobDetail(runnable),
TriggerUtils.makeImmediateTrigger(generateNameForInstance(runnable), 0, 0l));
}
catch (Exception e) {
throw new QuartzSchedulingException(e);
}
}
public void shutdown(boolean waitForTasksToCompleteOnShutdown) {
try {
this.scheduler.shutdown(waitForTasksToCompleteOnShutdown);
}
catch (SchedulerException e) {
throw new QuartzSchedulingException(e);
}
}
public ScheduledFuture<?> scheduleWithInitialDelay(Runnable runnable, long initialDelay, TimeUnit timeUnit)
throws Exception {
getFutureDate(initialDelay, timeUnit);
Trigger initialDelayTrigger = new SimpleTrigger(generateNameForInstance(runnable), Scheduler.DEFAULT_GROUP,
getFutureDate(initialDelay, timeUnit));
return scheduleWithTrigger(runnable, initialDelayTrigger, null);
}
public ScheduledFuture<?> scheduleAtFixedRate(Runnable runnable, long initialDelay, long period, TimeUnit timeUnit)
throws Exception {
Trigger fixedRateTrigger = new SimpleTrigger(generateNameForInstance(runnable), Scheduler.DEFAULT_GROUP,
getFutureDate(initialDelay, timeUnit), null, SimpleTrigger.REPEAT_INDEFINITELY,
TimeUnit.MILLISECONDS.convert(period, timeUnit));
return scheduleWithTrigger(runnable, fixedRateTrigger, null);
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable runnable, long initialDelay, long delay,
TimeUnit timeUnit) throws Exception {
getFutureDate(initialDelay, timeUnit);
Trigger fixedDelayTrigger = createFixedDelayTrigger(runnable, initialDelay, delay, timeUnit);
return scheduleWithTrigger(runnable, fixedDelayTrigger, new String[]{this.fixedDelayJobListener.getName()});
}
public ScheduledFuture<?> scheduleWithCronExpression(Runnable runnable, String cronExpression)
throws Exception {
return scheduleWithTrigger(runnable, new CronTrigger(generateNameForInstance(runnable), Scheduler.DEFAULT_GROUP,
cronExpression), null);
}
private ScheduledFuture<?> scheduleWithTrigger(Runnable runnable, Trigger fixedRateTrigger,
String[] jobListenerNames) throws Exception {
JobDetail jobDetail = wrapAsJobDetail(runnable);
if (null != jobListenerNames) {
for (String jobListenerName : jobListenerNames) {
jobDetail.addJobListener(jobListenerName);
}
}
this.scheduler.scheduleJob(jobDetail, fixedRateTrigger);
return new ScheduledFutureJobWrapper(scheduler, jobDetail, fixedRateTrigger);
}
private static JobDetail wrapAsJobDetail(Runnable runnable) throws Exception {
MethodInvokingJobDetailFactoryBean factoryBean = new MethodInvokingJobDetailFactoryBean();
factoryBean.setTargetObject(runnable);
factoryBean.setTargetMethod(RUN_METHOD_NAME);
factoryBean.setBeanName(generateNameForInstance(runnable));
factoryBean.afterPropertiesSet();
JobDetail jobDetail = (JobDetail) factoryBean.getObject();
jobDetail.setJobClass(InterruptableMethodInvokingJob.class);
jobDetail.getJobDataMap().put(RUNNABLE_INSTANCE, runnable);
return jobDetail;
}
private static String generateNameForInstance(Object instance) {
return instance.getClass().getName() + "#" + System.identityHashCode(instance);
}
private static Date getFutureDate(long delay, TimeUnit timeUnit) {
return new Date(new Date().getTime() + TimeUnit.MILLISECONDS.convert(delay, timeUnit));
}
private static Trigger createFixedDelayTrigger(Runnable runnable, long initialDelay, long taskDelay, TimeUnit timeUnit) {
Trigger fixedDelayTrigger = new SimpleTrigger(generateNameForInstance(runnable), Scheduler.DEFAULT_GROUP,
getFutureDate(initialDelay, timeUnit));
fixedDelayTrigger.getJobDataMap().put(FIXED_DELAY_PARAMETER, taskDelay);
fixedDelayTrigger.getJobDataMap().put(TIME_UNIT_PARAMETER, timeUnit);
fixedDelayTrigger.getJobDataMap().put(TRIGGER_NAME_PARAMETER, fixedDelayTrigger.getName());
return fixedDelayTrigger;
}
/**
* Wrapper class for a Quartz {@link Job}, allowing running Quartz jobs top be manipulated via
* the {@link java.util.concurrent.ScheduledFuture} interface.
* It is designed to be used for periodic tasks, therefore get() either block or throw
* {@link java.util.concurrent.CancellationException}.
*/
private class ScheduledFutureJobWrapper implements ScheduledFuture<Object> {
private final Scheduler scheduler;
private final JobDetail jobDetail;
private final Trigger trigger;
private final CountDownLatch cancellationLatch = new CountDownLatch(1);
private ScheduledFutureJobWrapper(Scheduler scheduler, JobDetail jobDetail, Trigger trigger) {
this.scheduler = scheduler;
this.jobDetail = jobDetail;
this.trigger = trigger;
}
public long getDelay(TimeUnit unit) {
long nextFireTime = this.trigger.getNextFireTime().getTime();
long timeNow = new Date().getTime();
return nextFireTime > timeNow ? unit.convert(nextFireTime - timeNow, TimeUnit.MILLISECONDS) : 0;
}
public int compareTo(Delayed o) {
return new Long(getDelay(TimeUnit.MILLISECONDS)).compareTo(o.getDelay(TimeUnit.MILLISECONDS));
}
/*
* Synchronized for thread-safety. This is not supposed to be a highly concurrent operation, therefore
* contention should be minimal.
*/
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (this.cancellationLatch.getCount() == 0) {
return true;
}
try {
if (mayInterruptIfRunning) {
this.scheduler.interrupt(this.jobDetail.getName(), Scheduler.DEFAULT_GROUP);
}
if (this.scheduler.deleteJob(this.jobDetail.getName(), Scheduler.DEFAULT_GROUP)) {
this.cancellationLatch.countDown();
return true;
}
else {
return false;
}
}
catch (SchedulerException e) {
throw new QuartzSchedulingException(e);
}
}
public boolean isCancelled() {
return this.cancellationLatch.getCount() == 0;
}
public boolean isDone() {
return isCancelled();
}
public Object get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
this.cancellationLatch.await(timeout, unit);
throw new CancellationException();
}
public Object get() throws InterruptedException, ExecutionException {
this.cancellationLatch.await();
throw new CancellationException();
}
}
/**
* Wrapper class allowing for Quartz jobs to be interrupted.
*/
private static class InterruptableMethodInvokingJob extends MethodInvokingJobDetailFactoryBean.MethodInvokingJob
implements InterruptableJob {
private Thread executionThread;
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
this.executionThread = Thread.currentThread();
super.executeInternal(context);
}
public void interrupt() throws UnableToInterruptJobException {
this.executionThread.interrupt();
}
}
/**
* {@link org.quartz.JobListener} used for re-scheduling a fixed delay task, as Quartz does
* not support fixed delay tasks out of the box.
*/
public class FixedDelayJobListener extends JobListenerSupport {
private final String name;
public FixedDelayJobListener() {
this.name = generateNameForInstance(this);
}
public String getName() {
return name;
}
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
try {
Trigger trigger = createFixedDelayTrigger((Runnable) context.getMergedJobDataMap().get(
RUNNABLE_INSTANCE),
context.getMergedJobDataMap().getLong(FIXED_DELAY_PARAMETER),
context.getMergedJobDataMap().getLong(FIXED_DELAY_PARAMETER),
(TimeUnit) context.getMergedJobDataMap().get(TIME_UNIT_PARAMETER));
trigger.setJobGroup(context.getJobDetail().getGroup());
trigger.setJobName(context.getJobDetail().getName());
scheduler.rescheduleJob(context.getMergedJobDataMap().getString(TRIGGER_NAME_PARAMETER),
Scheduler.DEFAULT_GROUP, trigger);
}
catch (Exception e) {
throw new QuartzSchedulingException(e);
}
}
}
}

View File

@@ -16,15 +16,20 @@
package org.springframework.integration.quartz;
import org.junit.Test;
/**
* @author Mark Fisher
* @author Marius Bogoevici
*/
public class PlaceholderTests {
public class QuartzSchedulingException extends RuntimeException {
@Test
public void test() {
public QuartzSchedulingException() {
super();
}
public QuartzSchedulingException(Throwable cause) {
super(cause);
}
public QuartzSchedulingException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2002-2008 the original author or authors.
*
* 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.springframework.integration.quartz;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.scheduling.spi.ScheduleServiceProvider;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
/**
* An integration test for the Quartz-based implementation of the Scheduling SPI.
*
* @author Marius Bogoevici
*/
@ContextConfiguration(locations = "quartz-scheduler-context.xml")
public class TestQuartzScheduleServiceProvider extends AbstractJUnit4SpringContextTests {
@Autowired
private ScheduleServiceProvider scheduleServiceProvider;
@Autowired
private Scheduler scheduler;
@Test
@DirtiesContext
public void testExecute() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean hasRun = new AtomicBoolean(false);
scheduleServiceProvider.execute(new SimpleRunnable(latch, hasRun));
latch.await(500, TimeUnit.MILLISECONDS);
assertTrue(hasRun.get());
}
@Test
@DirtiesContext
public void testReturnedScheduledFutureCancelsJobWithoutInterruption() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
AtomicBoolean hasBeenInterrupted = new AtomicBoolean(false);
ScheduledFuture<?> scheduledFuture = scheduleServiceProvider.scheduleAtFixedRate(new SimpleRunnable(latch,
hasBeenInterrupted), 500, 500,
TimeUnit.MILLISECONDS);
// the job is scheduled
assertEquals(1, scheduler.getJobNames(Scheduler.DEFAULT_GROUP).length);
scheduledFuture.cancel(false);
// the job is not scheduled anymore
assertEquals(0, scheduler.getJobNames(Scheduler.DEFAULT_GROUP).length);
}
@Test
@DirtiesContext
public void testReturnedScheduledFutureCancelsJobWithInterruption() throws Exception {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
AtomicBoolean hasBeenInterrupted = new AtomicBoolean(false);
ScheduledFuture<?> scheduledFuture = scheduleServiceProvider.scheduleAtFixedRate(new LongRunningRunnable(startLatch,
endLatch, hasBeenInterrupted), 500, 10000, TimeUnit.MILLISECONDS);
// the job is scheduled
assertEquals(1, scheduler.getJobNames(Scheduler.DEFAULT_GROUP).length);
startLatch.await(1000, TimeUnit.MILLISECONDS);
scheduledFuture.cancel(true);
endLatch.await(1000, TimeUnit.MILLISECONDS);
// the job is not scheduled anymore
assertEquals(0, scheduler.getJobNames(Scheduler.DEFAULT_GROUP).length);
assertTrue(hasBeenInterrupted.get());
}
@Test
@DirtiesContext
public void testExecuteWithFixedRate() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
AtomicBoolean hasRun = new AtomicBoolean(false);
SimpleRunnable runnable = new SimpleRunnable(latch, hasRun);
scheduleServiceProvider.scheduleAtFixedRate(runnable, 500, 500, TimeUnit.MILLISECONDS);
latch.await(3000, TimeUnit.MILLISECONDS);
assertTrue(hasRun.get());
}
@Test
@DirtiesContext
public void testExecuteWithCron() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
AtomicBoolean hasRun = new AtomicBoolean(false);
SimpleRunnable runnable = new SimpleRunnable(latch, hasRun);
scheduleServiceProvider.scheduleWithCronExpression(runnable, "0/2 * * * * ?");
latch.await(6000, TimeUnit.MILLISECONDS);
assertTrue(hasRun.get());
}
@Test
@DirtiesContext
public void testExecuteWithFixedDelay() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
AtomicBoolean hasRun = new AtomicBoolean(false);
SimpleRunnable runnable = new SimpleRunnable(latch, hasRun);
scheduleServiceProvider.scheduleWithFixedDelay(runnable, 500, 500, TimeUnit.MILLISECONDS);
latch.await(2000, TimeUnit.MILLISECONDS);
assertTrue(hasRun.get());
}
private class SimpleRunnable implements Runnable {
private final CountDownLatch latch;
private final AtomicBoolean hasRun;
private final List<Long> executionTimes = new ArrayList<Long>();
private final long startTime;
public SimpleRunnable(CountDownLatch latch, AtomicBoolean hasRun) {
this.latch = latch;
this.hasRun = hasRun;
startTime = 0;
}
public List<Long> getExecutionTimes() {
return executionTimes;
}
public long getStartTime() {
return startTime;
}
public void run() {
hasRun.set(latch.getCount() == 1);
this.executionTimes.add(new Date().getTime());
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
latch.countDown();
}
}
private class LongRunningRunnable implements Runnable {
private final CountDownLatch startLatch;
private final CountDownLatch endLatch;
private final AtomicBoolean hasBeenInterrupted;
public LongRunningRunnable(CountDownLatch startLatch, CountDownLatch endLatch, AtomicBoolean hasBeenInterupted) {
this.startLatch = startLatch;
this.endLatch = endLatch;
this.hasBeenInterrupted = hasBeenInterupted;
}
public void run() {
// This method differentiates between the interruptions caused by the test and the external ones
try {
startLatch.countDown();
// Sleep for a long time, waiting for an interruption
try {
Thread.sleep(2000);
}
catch (InterruptedException e) {
hasBeenInterrupted.set(true);
Thread.currentThread().interrupt();
}
finally {
endLatch.countDown();
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="scheduleServiceProvider" class="org.springframework.integration.quartz.QuartzScheduleServiceProvider">
<constructor-arg ref="scheduler"/>
</bean>
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"/>
</beans>