package org.quartz; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import java.util.Collection; import java.util.Random; import java.util.concurrent.TimeUnit; import junit.framework.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.quartz.impl.DirectSchedulerFactory; import org.quartz.impl.SchedulerRepository; import org.quartz.impl.jdbcjobstore.JdbcQuartzTestUtilities; import org.quartz.impl.jdbcjobstore.JobStoreTX; import org.quartz.simpl.SimpleThreadPool; import org.quartz.utils.ConnectionProvider; import org.quartz.utils.DBConnectionManager; @RunWith(Parameterized.class) public class FlakyJdbcSchedulerTest extends AbstractSchedulerTest { @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{{0f, 0f, 0f}, {0.2f, 0f, 0f}, {0f, 0.2f, 0f}, {0f, 0f, 0.2f}, {0.2f, 0.2f, 0.2f}}); } private final Random rndm; private final float createFailureProb; private final float preCommitFailureProb; private final float postCommitFailureProb; public FlakyJdbcSchedulerTest(float createFailureProb, float preCommitFailureProb, float postCommitFailureProb) { this.createFailureProb = createFailureProb; this.preCommitFailureProb = preCommitFailureProb; this.postCommitFailureProb = postCommitFailureProb; this.rndm = new Random(); } @Override protected Scheduler createScheduler(String name, int threadPoolSize) throws SchedulerException { try { DBConnectionManager.getInstance().addConnectionProvider(name, new FlakyConnectionProvider(name)); } catch (SQLException ex) { throw new AssertionError(ex); } JobStoreTX jobStore = new JobStoreTX(); jobStore.setDataSource(name); jobStore.setTablePrefix("QRTZ_"); jobStore.setInstanceId("AUTO"); jobStore.setDbRetryInterval(50); DirectSchedulerFactory.getInstance().createScheduler(name + "Scheduler", "AUTO", new SimpleThreadPool(threadPoolSize, Thread.NORM_PRIORITY), jobStore, null, 0, -1, 50); return SchedulerRepository.getInstance().lookup(name + "Scheduler"); } @Test public void testTriggerFiring() throws Exception { final int jobCount = 100; final int execCount = 5; Scheduler scheduler = createScheduler("testTriggerFiring", 2); try { for (int i = 0; i < jobCount; i++) { String jobName = "myJob" + i; JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(jobName, "myJobGroup") .usingJobData("data", 0).storeDurably().requestRecovery().build(); Trigger trigger = TriggerBuilder .newTrigger() .withIdentity("triggerName" + i, "triggerGroup") .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1) .withRepeatCount(execCount - 1)).build(); if (!scheduler.checkExists(jobDetail.getKey())) { scheduler.scheduleJob(jobDetail, trigger); } } scheduler.start(); for (int i = 0; i < TimeUnit.MINUTES.toSeconds(5); i++) { int doneCount = 0; for (int j = 0; j < jobCount; j++) { JobDetail jobDetail = scheduler.getJobDetail(new JobKey("myJob" + i, "myJobGroup")); if (jobDetail.getJobDataMap().getInt("data") >= execCount) { doneCount++; } } if (doneCount == jobCount) { return; } TimeUnit.SECONDS.sleep(1); } Assert.fail(); } finally { scheduler.shutdown(true); } } @PersistJobDataAfterExecution @DisallowConcurrentExecution public static class TestJob implements Job { public void execute(JobExecutionContext context) { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); int val = dataMap.getInt("data") + 1; dataMap.put("data", val); } } private void createFailure() throws SQLException { if (rndm.nextFloat() < createFailureProb) { throw new SQLException("FlakyConnection failed on you on creation."); } } private void preCommitFailure() throws SQLException { if (rndm.nextFloat() < preCommitFailureProb) { throw new SQLException("FlakyConnection failed on you pre-commit."); } } private void postCommitFailure() throws SQLException { if (rndm.nextFloat() < postCommitFailureProb) { throw new SQLException("FlakyConnection failed on you post-commit."); } } private class FlakyConnectionProvider implements ConnectionProvider { private final Thread safeThread; private final String delegateName; private FlakyConnectionProvider(String name) throws SQLException { this.delegateName = "delegate_" + name; this.safeThread = Thread.currentThread(); JdbcQuartzTestUtilities.createDatabase(delegateName); } @Override public Connection getConnection() throws SQLException { if (Thread.currentThread() == safeThread) { return DBConnectionManager.getInstance().getConnection(delegateName); } else { createFailure(); return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] {Connection.class}, new FlakyConnectionInvocationHandler(DBConnectionManager.getInstance().getConnection(delegateName))); } } @Override public void shutdown() throws SQLException { DBConnectionManager.getInstance().shutdown(delegateName); JdbcQuartzTestUtilities.destroyDatabase(delegateName); JdbcQuartzTestUtilities.shutdownDatabase(); } @Override public void initialize() throws SQLException { //no-op } } private class FlakyConnectionInvocationHandler implements InvocationHandler { private final Connection delegate; public FlakyConnectionInvocationHandler(Connection delegate) { this.delegate = delegate; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("commit".equals(method.getName())) { preCommitFailure(); method.invoke(delegate, args); postCommitFailure(); return null; } else { return method.invoke(delegate, args); } } } }