Merge pull request #749 from apache/TOMEE-2937_EjbCronTriggerEnd
TOMEE-2937 Make sure endDate in the past does not fail
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/CalendarTimerData.java b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/CalendarTimerData.java
index 975b804..8fb3199 100644
--- a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/CalendarTimerData.java
+++ b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/CalendarTimerData.java
@@ -17,6 +17,7 @@
package org.apache.openejb.core.timer;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.openejb.core.timer.EJBCronTrigger.ParseException;
import org.apache.openejb.quartz.impl.triggers.AbstractTrigger;
@@ -83,6 +84,6 @@
@Override
public String toString() {
- return TimerType.Calendar.name() + " scheduleExpression = [" + scheduleExpression + "]";
+ return TimerType.Calendar.name() + " scheduleExpression = [" + ToStringBuilder.reflectionToString(scheduleExpression) + "]";
}
}
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EJBCronTrigger.java b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EJBCronTrigger.java
index 52b6b16..4b4e9f3 100644
--- a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EJBCronTrigger.java
+++ b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EJBCronTrigger.java
@@ -104,6 +104,7 @@
private final TimeZone timezone;
private final String rawValue;
+ private final boolean isExpired;
public EJBCronTrigger(final ScheduleExpression expr) throws ParseException {
@@ -118,7 +119,32 @@
timezone = expr.getTimezone() == null ? TimeZone.getDefault() : TimeZone.getTimeZone(expr.getTimezone());
setStartTime(expr.getStart() == null ? new Date() : expr.getStart());
- setEndTime(expr.getEnd());
+
+ /*
+ * @testName: endNeverExpires
+ *
+ * @test_Strategy: create a timer with year="currentYear - currentYear+1", and
+ * end="currentYear-1". The end value is prior to the year values, and this
+ * timer will never expire. Creating this timer will succeed, but any timer
+ * method access in a subsequent business method will result in
+ * NoSuchObjectLocalException.
+ *
+ * EJB32 TCK test tries to create an already expired Timer and it's supposed to not fail.
+ * This may happen whe you restart an application for instance.
+ * On the other hand, Quartz does not allow endTime to be before StartTime so we need to check first so we don't
+ * set the endDate but we flag up this timer as being expired.
+ *
+ * When the first time is computed we will fail and as a consequence TimerData will be flagged up as being expired.
+ * So if endDate is not set or endTime after startTime, then we can consider this timer as not expired.
+ * If endTime is set and it's before startTime, we swallow setEndTime to Quartz and set the expired flag to true
+ */
+ if (expr.getEnd() == null || !isBefore(expr.getEnd(), getStartTime())) {
+ setEndTime(expr.getEnd());
+ isExpired = false;
+
+ } else {
+ isExpired = true;
+ }
// If parsing fails on a field, record the error and move to the next field
final Map<Integer, ParseException> errors = new HashMap<>();
@@ -143,6 +169,10 @@
+ DELIMITER + expr.getHour() + DELIMITER + expr.getMinute() + DELIMITER + expr.getSecond();
}
+ private boolean isBefore(final Date end, final Date start) {
+ return start != null && end != null && start.after(end);
+ }
+
/**
* Computes a set of allowed values for the given field of a calendar based
* time expression.
@@ -354,7 +384,7 @@
} else if (currentFieldIndex >= 1) {
// No suitable value was found, so move back to the previous field
// and decrease the value
- final int maxAffectedFieldType = upadteCalendar(calendar, expressions[currentFieldIndex - 1].field, -1);
+ final int maxAffectedFieldType = updateCalendar(calendar, expressions[currentFieldIndex - 1].field, -1);
currentFieldIndex = CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.get(maxAffectedFieldType);
resetFields(calendar, maxAffectedFieldType, true);
} else {
@@ -368,6 +398,16 @@
}
@Override
+ public Date computeFirstFireTime(final org.apache.openejb.quartz.Calendar calendar) {
+ // timer may be expired up on creation (see constructor comments)
+ if (isExpired) {
+ throw new TimerExpiredException(String.format("Timer %s expired.", this));
+ }
+
+ return super.computeFirstFireTime(calendar);
+ }
+
+ @Override
public Date getFireTimeAfter(final Date afterTime) {
log.debug("start to getFireTimeAfter:" + afterTime);
final Calendar calendar = new GregorianCalendar(timezone);
@@ -447,7 +487,7 @@
// When current field is HOUR_OF_DAY, its upper field is DAY_OF_MONTH, so we need to -2 due to
// DAY_OF_WEEK.
final int parentFieldIndex = currentFieldIndex == 4 ? currentFieldIndex - 2 : currentFieldIndex - 1;
- final int maxAffectedFieldType = upadteCalendar(calendar, expressions[parentFieldIndex].field, 1);
+ final int maxAffectedFieldType = updateCalendar(calendar, expressions[parentFieldIndex].field, 1);
currentFieldIndex = CALENDAR_FIELD_TYPE_ORDERED_INDEX_MAP.get(maxAffectedFieldType);
resetFields(calendar, maxAffectedFieldType, false);
}
@@ -491,7 +531,7 @@
* @param field
* @return
*/
- private int upadteCalendar(final Calendar calendar, final int field, final int amount) {
+ private int updateCalendar(final Calendar calendar, final int field, final int amount) {
final Calendar old = new GregorianCalendar(timezone);
old.setTime(calendar.getTime());
calendar.add(field, amount);
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java
index 22f5b00..effee7c 100644
--- a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java
+++ b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java
@@ -773,7 +773,7 @@
try {
transactionManager.begin();
} catch (final Exception e) {
- log.warning("Exception occured while starting container transaction", e);
+ log.warning("Exception occurred while starting container transaction", e);
return;
}
}
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java
index 0ae1403..5c3d5b6 100644
--- a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java
+++ b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java
@@ -219,13 +219,19 @@
public void newTimer() {
//Initialize the Quartz Trigger
- trigger = initializeTrigger();
- trigger.computeFirstFireTime(null);
- trigger.setGroup(OPEN_EJB_TIMEOUT_TRIGGER_GROUP_NAME);
- trigger.setName(OPEN_EJB_TIMEOUT_TRIGGER_NAME_PREFIX + deploymentId + "_" + id);
- newTimer = true;
try {
+ trigger = initializeTrigger();
+ trigger.computeFirstFireTime(null);
+ trigger.setGroup(OPEN_EJB_TIMEOUT_TRIGGER_GROUP_NAME);
+ trigger.setName(OPEN_EJB_TIMEOUT_TRIGGER_NAME_PREFIX + deploymentId + "_" + id);
+ newTimer = true;
+
registerTimerDataSynchronization();
+
+ } catch (final TimerExpiredException e) {
+ setExpired(true);
+ log.warning("Timer {} is expired and will never trigger.", trigger);
+
} catch (final TimerStoreException e) {
throw new EJBException("Failed to register new timer data synchronization", e);
}
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerExpiredException.java b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerExpiredException.java
new file mode 100644
index 0000000..ec8f7e6
--- /dev/null
+++ b/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerExpiredException.java
@@ -0,0 +1,23 @@
+/*
+ * 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.openejb.core.timer;
+
+public class TimerExpiredException extends RuntimeException {
+ public TimerExpiredException(final String message) {
+ super(message);
+ }
+}
diff --git a/container/openejb-core/src/test/java/org/apache/openejb/timer/EJBCronTriggerTest.java b/container/openejb-core/src/test/java/org/apache/openejb/timer/EJBCronTriggerTest.java
index e1fdcb3..0cf244a 100644
--- a/container/openejb-core/src/test/java/org/apache/openejb/timer/EJBCronTriggerTest.java
+++ b/container/openejb-core/src/test/java/org/apache/openejb/timer/EJBCronTriggerTest.java
@@ -19,6 +19,7 @@
import org.apache.openejb.core.timer.EJBCronTrigger;
import org.apache.openejb.core.timer.EJBCronTrigger.ParseException;
+import org.apache.openejb.core.timer.TimerExpiredException;
import org.junit.Test;
import javax.ejb.ScheduleExpression;
@@ -27,12 +28,28 @@
import java.util.GregorianCalendar;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class EJBCronTriggerTest {
+ @Test
+ public void shouldBeAbleToCreateExpiredTrigger() throws ParseException {
+ final ScheduleExpression expr = new ScheduleExpression().year(2008).month(12).dayOfMonth(1).end(new Date(0));
+ final EJBCronTrigger trigger = new EJBCronTrigger(expr);
+ assertNotNull(trigger);
+ }
+
+ @Test(expected = TimerExpiredException.class)
+ public void computeFailsOnExpiredTriggers() throws ParseException {
+ final ScheduleExpression expr = new ScheduleExpression().year(2008).month(12).dayOfMonth(1).end(new Date(0));
+ final EJBCronTrigger trigger = new EJBCronTrigger(expr);
+ assertNotNull(trigger);
+ trigger.computeFirstFireTime(null);
+ }
+
@Test(timeout = 1000)
public void testSimpleDate() throws ParseException {
final ScheduleExpression expr = new ScheduleExpression().year(2008).month(12).dayOfMonth(1).start(new Date(0));
diff --git a/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java b/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java
index 9f9252a..1390e48 100644
--- a/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java
+++ b/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java
@@ -16,10 +16,10 @@
*/
package org.apache.openejb.timer;
+import org.apache.commons.lang3.time.DateUtils;
import org.apache.openejb.jee.EnterpriseBean;
-import org.apache.openejb.jee.SingletonBean;
import org.apache.openejb.junit.ApplicationComposer;
-import org.apache.openejb.testing.Module;
+import org.apache.openejb.testing.Classes;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -29,6 +29,9 @@
import javax.ejb.EJB;
import javax.ejb.Lock;
import javax.ejb.LockType;
+import javax.ejb.NoMoreTimeoutsException;
+import javax.ejb.NoSuchObjectLocalException;
+import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
@@ -36,24 +39,71 @@
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
+import java.util.Calendar;
+import java.util.Date;
+
import static org.junit.Assert.assertEquals;
@RunWith(ApplicationComposer.class)
+@Classes(innerClassesAsBean = true)
public class InitialIntervalTimerTest {
- @Module
- public EnterpriseBean bean() {
- return new SingletonBean(TimerWithDelay.class).localBean();
- }
@EJB
private TimerWithDelay bean;
+ @EJB
+ private TimerNeverExpires scheduleBean;
+
@Test
public void test() throws InterruptedException {
Thread.sleep(5400);
assertEquals(3, bean.getOk());
}
+ @Test
+ public void endNeverExpires() {
+ final Calendar cal = Calendar.getInstance();
+ final int currentYear = getForSchedule(Calendar.YEAR, cal);
+ cal.add(Calendar.SECOND, 10);
+ final ScheduleExpression exp = getPreciseScheduleExpression(cal);
+ final Date end = DateUtils.setYears(cal.getTime(), cal.get(Calendar.YEAR) - 1);
+ exp.end(end);
+ exp.year((currentYear) + " - " + (currentYear + 1));
+
+ final Timer timer = scheduleBean.createTimer(exp);
+ scheduleBean.passIfNoMoreTimeouts(timer);
+ }
+
+ public static ScheduleExpression getPreciseScheduleExpression(final Calendar... cals) {
+ Calendar cal = (cals.length == 0) ? Calendar.getInstance() : cals[0];
+ return new ScheduleExpression()
+ .year(cal.get(Calendar.YEAR)).month(getForSchedule(Calendar.MONTH, cal))
+ .dayOfMonth(getForSchedule(Calendar.DAY_OF_MONTH, cal))
+ .hour(getForSchedule(Calendar.HOUR_OF_DAY, cal))
+ .minute(getForSchedule(Calendar.MINUTE, cal))
+ .second(getForSchedule(Calendar.SECOND, cal));
+ }
+
+ public static int getForSchedule(final int field, final Calendar... calendars) {
+ int result = 0;
+ Calendar cal = null;
+ if (calendars.length == 0) {
+ cal = Calendar.getInstance();
+ } else {
+ cal = calendars[0];
+ }
+ result = cal.get(field);
+ if (field == Calendar.DAY_OF_WEEK) {
+ result--; // 0 and 7 are both Sunday
+ if (result == 0) {
+ result = (Math.random() < 0.5) ? 0 : 7;
+ }
+ } else if (field == Calendar.MONTH) {
+ result++;
+ }
+ return result;
+ }
+
@Singleton
@Startup
@Lock(LockType.READ)
@@ -85,4 +135,58 @@
timer.cancel();
}
}
+
+ @Singleton
+ @Startup
+ @Lock(LockType.READ)
+ public static class TimerNeverExpires {
+
+ @Resource
+ private TimerService timerService;
+
+ private int ok = 0;
+
+ @Timeout
+ public void timeout(final Timer timer) {
+ ok++;
+ }
+
+ public Timer createTimer(final ScheduleExpression exp) {
+ return timerService.createCalendarTimer(exp, new TimerConfig("TimerNeverExpires", false));
+ }
+
+ public String passIfNoMoreTimeouts(final Timer t) {
+ String result = "";
+ try {
+ Date nextTimeout = t.getNextTimeout();
+ throw new RuntimeException(
+ "Expecting NoSuchObjectLocalException or NoMoreTimeoutsException "
+ + "when accessing getNextTimeout, but actual " + nextTimeout);
+
+ } catch (final NoMoreTimeoutsException e) {
+ result += " Got expected " + e;
+
+ } catch (final NoSuchObjectLocalException e) {
+ result += " Got expected " + e;
+
+ }
+
+ try {
+ long timeRemaining = t.getTimeRemaining();
+ throw new RuntimeException(
+ "Expecting NoSuchObjectLocalException or NoMoreTimeoutsException "
+ + "when accessing getTimeRemaining, but actual " + timeRemaining);
+
+ } catch (final NoMoreTimeoutsException e) {
+ result += " Got expected " + e;
+
+ } catch (final NoSuchObjectLocalException e) {
+ result += " Got expected " + e;
+
+ }
+ return result;
+ }
+
+
+ }
}