diff --git a/quartz/build.gradle b/quartz/build.gradle
index 56b07b416..c139c00ce 100644
--- a/quartz/build.gradle
+++ b/quartz/build.gradle
@@ -42,6 +42,8 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.mockito:mockito-core:5.14.2'
+ testImplementation 'org.testcontainers:mssqlserver:1.20.3'
+ testImplementation 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java
index bbbbbf9ec..70bead214 100644
--- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java
+++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java
@@ -18,11 +18,20 @@
package org.quartz.impl.jdbcjobstore;
+import static org.quartz.TriggerKey.triggerKey;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.quartz.TriggerKey;
/**
*
@@ -80,6 +89,44 @@ protected Object getJobDataFromBlob(ResultSet rs, String colName)
}
return getObjectFromBlob(rs, colName);
}
+
+ @Override
+ public List selectTriggerToAcquire(Connection conn, long noLaterThan, long noEarlierThan, int maxCount)
+ throws SQLException {
+ // Set max rows to retrieve
+ if (maxCount < 1)
+ maxCount = 1; // we want at least one trigger back.
+ String selectTriggerToAcquire = "SELECT TOP " + maxCount + " " + SELECT_NEXT_TRIGGER_TO_ACQUIRE.substring(6);
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ List nextTriggers = new LinkedList<>();
+ try {
+ ps = conn.prepareStatement(rtp(selectTriggerToAcquire));
+
+ ps.setMaxRows(maxCount);
+
+ // Try to give jdbc driver a hint to hopefully not pull over more than the few rows we actually need.
+ // Note: in some jdbc drivers, such as MySQL, you must set maxRows before fetchSize, or you get exception!
+ ps.setFetchSize(maxCount);
+
+ ps.setString(1, STATE_WAITING);
+ ps.setBigDecimal(2, new BigDecimal(String.valueOf(noLaterThan)));
+ ps.setBigDecimal(3, new BigDecimal(String.valueOf(noEarlierThan)));
+ rs = ps.executeQuery();
+
+ while (rs.next() && nextTriggers.size() < maxCount) {
+ nextTriggers.add(triggerKey(
+ rs.getString(COL_TRIGGER_NAME),
+ rs.getString(COL_TRIGGER_GROUP)));
+ }
+
+ return nextTriggers;
+ } finally {
+ closeResultSet(rs);
+ closeStatement(ps);
+ }
+ }
+
}
// EOF
diff --git a/quartz/src/test/java/org/quartz/impl/jdbcjobstore/JdbcQuartzMSSQLUtilities.java b/quartz/src/test/java/org/quartz/impl/jdbcjobstore/JdbcQuartzMSSQLUtilities.java
new file mode 100644
index 000000000..23a696d99
--- /dev/null
+++ b/quartz/src/test/java/org/quartz/impl/jdbcjobstore/JdbcQuartzMSSQLUtilities.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright Super iPaaS Integration LLC, an IBM Company 2024
+ *
+ * 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.impl.jdbcjobstore;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import org.testcontainers.containers.MSSQLServerContainer;
+
+/**
+ * A utility class to create a database for Quartz MSSQL test.
+ *
+ * @author Arnaud Mergey
+ */
+
+public final class JdbcQuartzMSSQLUtilities {
+ private static final List DATABASE_SETUP_STATEMENTS;
+ static {
+ List setup = new ArrayList();
+ String setupScript;
+ try {
+ InputStream setupStream = MSSQLDelegate.class.getClassLoader()
+ .getResourceAsStream("org/quartz/impl/jdbcjobstore/tables_sqlServer.sql");
+ try {
+ BufferedReader r = new BufferedReader(new InputStreamReader(setupStream, "US-ASCII"));
+ StringBuilder sb = new StringBuilder();
+ while (true) {
+ String line = r.readLine();
+ if (line == null) {
+ break;
+ } else if (!line.startsWith("--")) {
+ sb.append(line.replace("GO", ";").replace("[enter_db_name_here]", "[master]")).append("\n");
+ }
+ }
+ setupScript = sb.toString();
+ } finally {
+ setupStream.close();
+ }
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ for (String command : setupScript.split(";")) {
+ if (!command.matches("\\s*")) {
+ setup.add(command);
+ }
+ }
+ DATABASE_SETUP_STATEMENTS = setup;
+ }
+
+ public static void createDatabase(MSSQLServerContainer> container) throws SQLException {
+ Connection conn = container.createConnection("");
+ try {
+ Statement statement = conn.createStatement();
+ for (String command : DATABASE_SETUP_STATEMENTS) {
+ statement.addBatch(command);
+ }
+ statement.executeBatch();
+ } finally {
+ conn.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLDatabaseCronTriggerTest.java b/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLDatabaseCronTriggerTest.java
new file mode 100644
index 000000000..23350972c
--- /dev/null
+++ b/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLDatabaseCronTriggerTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright Super iPaaS Integration LLC, an IBM Company 2024
+ *
+ * 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.integrations.tests;
+import java.util.Collections;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.quartz.*;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
+import static org.quartz.integrations.tests.TrackingJob.SCHEDULED_TIMES_KEY;
+/**
+ * A integration test for Quartz MSSQL Database Scheduler with Cron Trigger.
+ *
+ * @author Arnaud Mergey
+ */
+public class QuartzMSSQLDatabaseCronTriggerTest extends QuartzMSSQLTestSupport {
+ @Test
+ void testCronRepeatCount() throws Exception {
+ CronTrigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity("test")
+ .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ?"))
+ .build();
+ List scheduledTimes = Collections.synchronizedList(new LinkedList());
+ scheduler.getContext().put(SCHEDULED_TIMES_KEY, scheduledTimes);
+ JobDetail jobDetail = JobBuilder.newJob(TrackingJob.class).withIdentity("test").build();
+ scheduler.scheduleJob(jobDetail, trigger);
+
+ for (int i = 0; i < 20 && scheduledTimes.size() < 3; i++) {
+ Thread.sleep(500);
+ }
+ assertThat(scheduledTimes, hasSize(greaterThanOrEqualTo(3)));
+
+ Long[] times = scheduledTimes.toArray(new Long[scheduledTimes.size()]);
+ long baseline = times[0];
+ assertThat(baseline % 1000, is(0L));
+ for (int i = 1; i < times.length; i++) {
+ assertThat(times[i], is(baseline + TimeUnit.SECONDS.toMillis(i)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLTestSupport.java b/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLTestSupport.java
new file mode 100644
index 000000000..6f29cb552
--- /dev/null
+++ b/quartz/src/test/java/org/quartz/integrations/tests/QuartzMSSQLTestSupport.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright Super iPaaS Integration LLC, an IBM Company 2024
+ *
+ * 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.integrations.tests;
+
+import java.sql.SQLException;
+import java.util.Properties;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.quartz.impl.jdbcjobstore.JdbcQuartzMSSQLUtilities;
+import org.quartz.impl.jdbcjobstore.MSSQLDelegate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.MSSQLServerContainer;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A base class to support database (MSSQL) scheduler integration testing. Each
+ * test will have a fresh scheduler created and started, and it will auto
+ * shutdown upon each test run. The database will be created with schema before
+ * class and destroy after class test.
+ *
+ * @author Arnaud Mergey
+ */
+public class QuartzMSSQLTestSupport extends QuartzMemoryTestSupport {
+ protected static final Logger LOG = LoggerFactory.getLogger(QuartzMSSQLTestSupport.class);
+ static MSSQLServerContainer> mssqlserver = new MSSQLServerContainer(
+ DockerImageName.parse(MSSQLServerContainer.IMAGE).withTag("latest")).acceptLicense();
+
+ @BeforeAll
+ public static void initialize() throws Exception {
+ LOG.info("Starting MSSQL database.");
+ mssqlserver.start();
+ LOG.info("Database started.");
+ try {
+ LOG.info("Creating Database tables for Quartz.");
+ JdbcQuartzMSSQLUtilities.createDatabase(mssqlserver);
+ LOG.info("Database tables created.");
+ } catch (SQLException e) {
+ throw new Exception("Failed to create Quartz tables.", e);
+ }
+ }
+
+ @AfterAll
+ public static void shutdownDb() throws Exception {
+ mssqlserver.stop();
+ LOG.info("Database shutdown.");
+ }
+
+ protected Properties createSchedulerProperties() {
+ Properties properties = new Properties();
+ properties.put("org.quartz.scheduler.instanceName", "TestScheduler");
+ properties.put("org.quartz.scheduler.instanceId", "AUTO");
+ properties.put("org.quartz.scheduler.skipUpdateCheck", "true");
+ properties.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
+ properties.put("org.quartz.threadPool.threadCount", "12");
+ properties.put("org.quartz.threadPool.threadPriority", "5");
+ properties.put("org.quartz.jobStore.misfireThreshold", "10000");
+ properties.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
+ properties.put("org.quartz.jobStore.driverDelegateClass", MSSQLDelegate.class.getName());
+ properties.put("org.quartz.jobStore.useProperties", "true");
+ properties.put("org.quartz.jobStore.dataSource", "myDS");
+ properties.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
+ properties.put("org.quartz.jobStore.isClustered", "false");
+ properties.put("org.quartz.dataSource.myDS.driver", mssqlserver.getDriverClassName());
+ properties.put("org.quartz.dataSource.myDS.URL", mssqlserver.getJdbcUrl());
+ properties.put("org.quartz.dataSource.myDS.user", mssqlserver.getUsername());
+ properties.put("org.quartz.dataSource.myDS.password", mssqlserver.getPassword());
+ properties.put("org.quartz.dataSource.myDS.maxConnections", "5");
+ properties.put("org.quartz.dataSource.myDS.provider", "hikaricp");
+ return properties;
+ }
+}
\ No newline at end of file