From 99da2b85fbd897c5c52cff86eb285aedbdfb9b5e Mon Sep 17 00:00:00 2001 From: Prasanth Rao Date: Mon, 20 Nov 2023 13:12:55 +0530 Subject: [PATCH 1/4] CAMEL-20115: Support for Start Date and End Date in camel-quartz --- .../src/main/docs/quartz-component.adoc | 16 ++++++ .../camel/component/quartz/CamelJob.java | 26 +++++++++ .../component/quartz/QuartzEndpoint.java | 33 +++++++++--- ...artzCronRouteWithStartDateEndDateTest.java | 53 +++++++++++++++++++ 4 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java diff --git a/components/camel-quartz/src/main/docs/quartz-component.adoc b/components/camel-quartz/src/main/docs/quartz-component.adoc index 85b42b1f984e8..4ad85a8653324 100644 --- a/components/camel-quartz/src/main/docs/quartz-component.adoc +++ b/components/camel-quartz/src/main/docs/quartz-component.adoc @@ -164,6 +164,22 @@ quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.timeZone=Europ The timeZone value is the values accepted by `java.util.TimeZone`. +== Specifying start date + +The Quartz Scheduler allows you to configure start date per trigger. You can provide the start date as TimeInMillis as follows: + +---- +quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.startAt=1700452232554 +---- + +== Specifying end date + +The Quartz Scheduler allows you to configure end date per trigger. You can provide the end date as TimeInMillis as follows: + +---- +quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.endAt=1700452232554 +---- + == Configuring misfire instructions The quartz scheduler can be configured with a misfire instruction diff --git a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java index 13e3181dca348..b54ca895884d2 100644 --- a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java +++ b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.quartz; +import java.util.Date; import java.util.Collection; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReference; @@ -53,6 +54,12 @@ public class CamelJob implements Job, InterruptableJob { public void execute(JobExecutionContext context) throws JobExecutionException { Exchange exchange = null; try { + if (hasTriggerExpired(context)) { + LOG.warn("Trigger exists outside StartTime={} and EndTime={}. Skipping CamelJob jobExecutionContext={}", + context.getTrigger().getStartTime(), context.getTrigger().getEndTime(), context); + return; + } + if (LOG.isDebugEnabled()) { LOG.debug("Running CamelJob jobExecutionContext={}", context); } @@ -94,6 +101,25 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } } + /** + * Validates if the Fire Time lies within the Start Time and End Time + * + * @param context + * + * @return + */ + private boolean hasTriggerExpired(JobExecutionContext context) { + Date fireTime = context.getFireTime(); + + // Trigger valid if Start Time is null or before Fire Time + boolean validStartTime = context.getTrigger().getStartTime() == null || fireTime.after(context.getTrigger().getStartTime()); + + // Trigger valid if End Time is null or after Fire Time + boolean validEndTime = context.getTrigger().getEndTime() == null || fireTime.before(context.getTrigger().getEndTime()); + + return !(validStartTime && validEndTime); + } + protected CamelContext getCamelContext(JobExecutionContext context) throws JobExecutionException { SchedulerContext schedulerContext = getSchedulerContext(context); String camelContextName = context.getMergedJobDataMap().getString(QuartzConstants.QUARTZ_CAMEL_CONTEXT_NAME); diff --git a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java index cd2297a5b459c..e6951f1652b3f 100644 --- a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java +++ b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java @@ -382,14 +382,7 @@ private void addJobInScheduler() throws Exception { } } else { try { - // calculate whether the trigger can be triggered in the future - Calendar cal = null; - if (trigger.getCalendarName() != null) { - cal = scheduler.getCalendar(trigger.getCalendarName()); - } - OperableTrigger ot = (OperableTrigger) trigger; - Date ft = ot.computeFirstFireTime(cal); - if (ft == null && ignoreExpiredNextFireTime) { + if (hasTriggerExpired(scheduler, trigger)) { scheduled = false; LOG.warn( "Job {} (cron={}, triggerType={}, jobClass={}) not scheduled, because it will never fire in the future", @@ -433,6 +426,22 @@ private void addJobInScheduler() throws Exception { jobAdded.set(true); } + private boolean hasTriggerExpired(Scheduler scheduler, Trigger trigger) throws SchedulerException { + Calendar cal = null; + if (trigger.getCalendarName() != null) { + cal = scheduler.getCalendar(trigger.getCalendarName()); + } + OperableTrigger ot = (OperableTrigger) trigger; + + // check if current time is past the Trigger EndDate + if (ot.getEndTime() != null && new Date().after(ot.getEndTime())) { + return true; + } + // calculate whether the trigger can be triggered in the future + Date ft = ot.computeFirstFireTime(cal); + return (ft == null && ignoreExpiredNextFireTime); + } + private boolean hasTriggerChanged(Trigger oldTrigger, Trigger newTrigger) { if (newTrigger instanceof CronTrigger && oldTrigger instanceof CronTrigger) { CronTrigger newCron = (CronTrigger) newTrigger; @@ -471,6 +480,14 @@ private Trigger createTrigger(JobDetail jobDetail) throws Exception { } if (cron != null) { LOG.debug("Creating CronTrigger: {}", cron); + final String startAt = (String) copy.get("startAt"); + if (startAt != null) { + triggerBuilder.startAt(new Date(Long.parseLong(startAt))); + } + final String endAt = (String) copy.get("endAt"); + if (endAt != null) { + triggerBuilder.endAt(new Date(Long.parseLong(endAt))); + } final String timeZone = (String) copy.get("timeZone"); if (timeZone != null) { if (ObjectHelper.isNotEmpty(customCalendar)) { diff --git a/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java new file mode 100644 index 0000000000000..c054a3895d267 --- /dev/null +++ b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.camel.component.quartz; + +import java.util.concurrent.TimeUnit; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * This test the CronTrigger as a timer endpoint in a route. + */ +public class QuartzCronRouteWithStartDateEndDateTest extends BaseQuartzTest { + + @Test + public void testQuartzCronRouteWithStartDateEndDateTest() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMinimumMessageCount(2); + mock.await(5, TimeUnit.SECONDS); + + MockEndpoint.assertIsSatisfied(context); + assertThat(mock.getReceivedExchanges().size() <= 3, CoreMatchers.is(true)); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + // triggers every 1th second at precise 00,01,02,03..59 with startAt and endAt exactly 2 second apart. + // configuration will create a maximum of three messages + from("quartz://myGroup/myTimerName?cron=0/1 * * * * ?&trigger.startAt=" + System.currentTimeMillis() + + "&trigger.endAt=" + (System.currentTimeMillis() + 2000)).to("mock:result"); + } + }; + } +} \ No newline at end of file From 09fba04a801ee2310d3edce05acbb12a79f48235 Mon Sep 17 00:00:00 2001 From: Prasanth Rao Date: Mon, 20 Nov 2023 21:26:07 +0530 Subject: [PATCH 2/4] Fix for exact match in CamelJob for fireTime --- .../apache/camel/component/quartz/CamelJob.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java index b54ca895884d2..4587513c6e119 100644 --- a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java +++ b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/CamelJob.java @@ -16,8 +16,8 @@ */ package org.apache.camel.component.quartz; -import java.util.Date; import java.util.Collection; +import java.util.Date; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReference; @@ -59,7 +59,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { context.getTrigger().getStartTime(), context.getTrigger().getEndTime(), context); return; } - + if (LOG.isDebugEnabled()) { LOG.debug("Running CamelJob jobExecutionContext={}", context); } @@ -104,7 +104,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { /** * Validates if the Fire Time lies within the Start Time and End Time * - * @param context + * @param context * * @return */ @@ -112,10 +112,14 @@ private boolean hasTriggerExpired(JobExecutionContext context) { Date fireTime = context.getFireTime(); // Trigger valid if Start Time is null or before Fire Time - boolean validStartTime = context.getTrigger().getStartTime() == null || fireTime.after(context.getTrigger().getStartTime()); + Date startTime = context.getTrigger().getStartTime(); + boolean validStartTime + = context.getTrigger().getStartTime() == null || fireTime.equals(startTime) || fireTime.after(startTime); // Trigger valid if End Time is null or after Fire Time - boolean validEndTime = context.getTrigger().getEndTime() == null || fireTime.before(context.getTrigger().getEndTime()); + Date endTime = context.getTrigger().getEndTime(); + boolean validEndTime + = context.getTrigger().getEndTime() == null || fireTime.equals(endTime) || fireTime.before(endTime); return !(validStartTime && validEndTime); } From 78dafcaf7c4e5fc217ada17accffb32a95ae5a9f Mon Sep 17 00:00:00 2001 From: Prasanth Rao Date: Wed, 22 Nov 2023 15:53:30 +0530 Subject: [PATCH 3/4] Move to Date with Timezone format --- .../src/main/docs/quartz-component.adoc | 8 ++++---- .../camel/component/quartz/QuartzEndpoint.java | 6 ++++-- .../QuartzCronRouteWithStartDateEndDateTest.java | 16 ++++++++++++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/components/camel-quartz/src/main/docs/quartz-component.adoc b/components/camel-quartz/src/main/docs/quartz-component.adoc index 4ad85a8653324..4e7d75e2017ec 100644 --- a/components/camel-quartz/src/main/docs/quartz-component.adoc +++ b/components/camel-quartz/src/main/docs/quartz-component.adoc @@ -166,18 +166,18 @@ The timeZone value is the values accepted by `java.util.TimeZone`. == Specifying start date -The Quartz Scheduler allows you to configure start date per trigger. You can provide the start date as TimeInMillis as follows: +The Quartz Scheduler allows you to configure start date per trigger. You can provide the start date in the date format yyyy-MM-dd'T'HH:mm:ssz. ---- -quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.startAt=1700452232554 +quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.startAt=2023-11-22T14:32:36UTC ---- == Specifying end date -The Quartz Scheduler allows you to configure end date per trigger. You can provide the end date as TimeInMillis as follows: +The Quartz Scheduler allows you to configure end date per trigger. You can provide the end date in the date format yyyy-MM-dd'T'HH:mm:ssz. ---- -quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.endAt=1700452232554 +quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.endAt=2023-11-22T14:32:36UTC ---- == Configuring misfire instructions diff --git a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java index e6951f1652b3f..667ca8c4fadfe 100644 --- a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java +++ b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzEndpoint.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.quartz; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; @@ -481,12 +482,13 @@ private Trigger createTrigger(JobDetail jobDetail) throws Exception { if (cron != null) { LOG.debug("Creating CronTrigger: {}", cron); final String startAt = (String) copy.get("startAt"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); if (startAt != null) { - triggerBuilder.startAt(new Date(Long.parseLong(startAt))); + triggerBuilder.startAt(dateFormat.parse(startAt)); } final String endAt = (String) copy.get("endAt"); if (endAt != null) { - triggerBuilder.endAt(new Date(Long.parseLong(endAt))); + triggerBuilder.endAt(dateFormat.parse(endAt)); } final String timeZone = (String) copy.get("timeZone"); if (timeZone != null) { diff --git a/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java index c054a3895d267..4a55c5a581be7 100644 --- a/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java +++ b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java @@ -16,6 +16,10 @@ */ package org.apache.camel.component.quartz; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; @@ -43,10 +47,18 @@ public void testQuartzCronRouteWithStartDateEndDateTest() throws Exception { protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { public void configure() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + calendar.add(Calendar.SECOND, 3); + Date startDate = calendar.getTime(); + calendar.add(Calendar.SECOND, 2); + Date endDate = calendar.getTime(); + // triggers every 1th second at precise 00,01,02,03..59 with startAt and endAt exactly 2 second apart. // configuration will create a maximum of three messages - from("quartz://myGroup/myTimerName?cron=0/1 * * * * ?&trigger.startAt=" + System.currentTimeMillis() - + "&trigger.endAt=" + (System.currentTimeMillis() + 2000)).to("mock:result"); + from("quartz://myGroup/myTimerName?cron=0/1 * * * * ?&trigger.startAt=" + dateFormat.format(startDate) + + "&trigger.endAt=" + dateFormat.format(endDate)).to("mock:result"); } }; } From 27c4a23d2641ade2b4da205e566ee3ad701cdf43 Mon Sep 17 00:00:00 2001 From: Prasanth Rao Date: Fri, 24 Nov 2023 15:19:53 +0530 Subject: [PATCH 4/4] Updated adoc and removed string concatenation from Test --- .../src/main/docs/quartz-component.adoc | 9 +++++++-- .../QuartzCronRouteWithStartDateEndDateTest.java | 14 +++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/components/camel-quartz/src/main/docs/quartz-component.adoc b/components/camel-quartz/src/main/docs/quartz-component.adoc index 4e7d75e2017ec..f97c3b52539fc 100644 --- a/components/camel-quartz/src/main/docs/quartz-component.adoc +++ b/components/camel-quartz/src/main/docs/quartz-component.adoc @@ -166,7 +166,8 @@ The timeZone value is the values accepted by `java.util.TimeZone`. == Specifying start date -The Quartz Scheduler allows you to configure start date per trigger. You can provide the start date in the date format yyyy-MM-dd'T'HH:mm:ssz. +The Quartz Scheduler allows you to configure start date per trigger. You can provide the start date +in the date format yyyy-MM-dd'T'HH:mm:ssz. ---- quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.startAt=2023-11-22T14:32:36UTC @@ -174,12 +175,16 @@ quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.startAt=2023-1 == Specifying end date -The Quartz Scheduler allows you to configure end date per trigger. You can provide the end date in the date format yyyy-MM-dd'T'HH:mm:ssz. +The Quartz Scheduler allows you to configure end date per trigger. You can provide the end date +in the date format yyyy-MM-dd'T'HH:mm:ssz. ---- quartz://groupName/timerName?cron=0+0/5+12-18+?+*+MON-FRI&trigger.endAt=2023-11-22T14:32:36UTC ---- +Note: Start and end dates may be affected by time drifts and unpredictable behavior during +daylight saving time changes. Exercise caution, especially in environments where precise timing is critical. + == Configuring misfire instructions The quartz scheduler can be configured with a misfire instruction diff --git a/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java index 4a55c5a581be7..b39084a207390 100644 --- a/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java +++ b/components/camel-quartz/src/test/java/org/apache/camel/component/quartz/QuartzCronRouteWithStartDateEndDateTest.java @@ -47,19 +47,19 @@ public void testQuartzCronRouteWithStartDateEndDateTest() throws Exception { protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { public void configure() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); - Calendar calendar = Calendar.getInstance(); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.add(Calendar.SECOND, 3); Date startDate = calendar.getTime(); calendar.add(Calendar.SECOND, 2); Date endDate = calendar.getTime(); - + // triggers every 1th second at precise 00,01,02,03..59 with startAt and endAt exactly 2 second apart. // configuration will create a maximum of three messages - from("quartz://myGroup/myTimerName?cron=0/1 * * * * ?&trigger.startAt=" + dateFormat.format(startDate) - + "&trigger.endAt=" + dateFormat.format(endDate)).to("mock:result"); + fromF("quartz://myGroup/myTimerName?cron=0/1 * * * * ?&trigger.startAt=%s&trigger.endAt=%s", + dateFormat.format(startDate), dateFormat.format(endDate)).to("mock:result"); } }; } -} \ No newline at end of file +}