Skip to content

Commit

Permalink
Fixed Issue with OFBiz Job Scheduler and Daylight Saving Time (OFBIZ-…
Browse files Browse the repository at this point in the history
…12864) (#674)

* Fixed recurring job not scheduling when DST change
The issue occurs when DST changes, and OFBiz fails to schedule recurring jobs properly. This is due to a condition in the PersistedServiceJob.createRecurrence method where it compares the next scheduled time (next) with the start time (startTime) for the job.
To address the issue, adding a new field named JobSandbox.runTimeEpoch.
This field would store the UTC format epoch milliseconds of the runtime date.
When scheduling or rescheduling recurring jobs, the system would use the UTC epoch stored in JobSandbox.runTimeEpoch for comparison.
This solution ensures that the system uses a consistent, UTC-based time for scheduling and rescheduling recurring jobs, even when DST changes affect the local time.
To implement this solution, you would need to:

Modify the PersistedServiceJob.createRecurrence method to calculate and store the UTC epoch milliseconds in the JobSandbox.runTimeEpoch field.

Update the code responsible for polling and rescheduling jobs to use the JobSandbox.runTimeEpoch field when it is set. If the field is not set, you would fall back to getting the runtime date to filter the jobs.

By using this approach, system should be able to handle recurring job scheduling more reliably, especially when DST changes are involved, as it ensures that all time comparisons are made in a consistent UTC format.

* Checkstyle fixes

* Store the runTimeEpoch while recurring job scheduling
  • Loading branch information
dixitdeepak authored Nov 2, 2023
1 parent 2e084e7 commit 46701da
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 6 deletions.
1 change: 1 addition & 0 deletions framework/service/entitydef/entitymodel.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ under the License.
<field name="jobId" type="id"></field>
<field name="jobName" type="name"></field>
<field name="runTime" type="date-time"></field>
<field name="runTimeEpoch" type="numeric"/><!-- Store the Epoch millisecond to avoid DST change issue (when clock set 1 hr back) -->
<field name="priority" type="numeric"></field>
<field name="poolId" type="name"></field>
<field name="statusId" type="id"></field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -179,8 +181,27 @@ protected List<Job> poll(int limit) {
return Collections.emptyList();
}
// basic query
List<EntityExpr> expressions = UtilMisc.toList(EntityCondition.makeCondition("runTime",
EntityOperator.LESS_THAN_EQUAL_TO, UtilDateTime.nowTimestamp()),
/*
By adding the runTimeEpoch field to handle DST changes in recurring job scheduling is a practical solution.
By storing the runtime epoch in UTC format, to make the system DST-aware, which helps prevent issues related to time changes,
especially when the clock is set back by 1 hour during the transition.
Additionally, keeping the runtime field while polling is a good practice,
as it ensures backward compatibility and provides flexibility in situations where the system sets the jobSandbox.runtime value.
This approach allows the system to work with both the new UTC-based runTimeEpoch field and the existing runtime field, as needed.
To summarize, by introducing the runTimeEpoch field and handling the transition between UTC epoch time and the runtime field,
to make recurring job scheduling more robust and DST-aware,
which should help prevent scheduling issues during Daylight Saving Time changes.
*/
List<EntityCondition> expressions = UtilMisc.toList(
EntityCondition.makeCondition(
EntityCondition.makeCondition("runTimeEpoch",
EntityOperator.LESS_THAN_EQUAL_TO, ZonedDateTime.now(ZoneId.of("UTC")).toInstant().toEpochMilli()),
EntityOperator.OR,
EntityCondition.makeCondition(
EntityCondition.makeCondition("runTimeEpoch", null),
EntityOperator.AND,
EntityCondition.makeCondition("runTime",
EntityOperator.LESS_THAN_EQUAL_TO, UtilDateTime.nowTimestamp()))),
EntityCondition.makeCondition("startDateTime", EntityOperator.EQUALS, null),
EntityCondition.makeCondition("cancelDateTime", EntityOperator.EQUALS, null),
EntityCondition.makeCondition("runByInstanceId", EntityOperator.EQUALS, null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@

import java.io.IOException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -61,6 +68,13 @@
public class PersistedServiceJob extends GenericServiceJob {

private static final String MODULE = PersistedServiceJob.class.getName();
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
.appendOptional(
new DateTimeFormatterBuilder()
.appendLiteral('.')
.appendValue(ChronoField.MICRO_OF_SECOND, 1, 3, SignStyle.NOT_NEGATIVE).toFormatter())
.toFormatter().withZone(ZonedDateTime.now().getZone());

private final transient Delegator delegator;
private long nextRecurrence = -1;
Expand All @@ -79,8 +93,14 @@ public PersistedServiceJob(DispatchContext dctx, GenericValue jobValue, GenericR
super(dctx, jobValue.getString("jobId"), jobValue.getString("jobName"), null, null, req);
this.delegator = dctx.getDelegator();
this.jobValue = jobValue;
Timestamp storedDate = jobValue.getTimestamp("runTime");
this.startTime = storedDate.getTime();
/*
This solution ensures that the system uses a consistent,
UTC-based time for scheduling and rescheduling recurring jobs, even when DST changes affect the local time.
*/
ZonedDateTime startTimeZD = ZonedDateTime.parse(
jobValue.getString("runTime"), FORMATTER).withZoneSameInstant(ZoneId.of("UTC"));
this.startTime = UtilValidate.isNotEmpty(jobValue.get("runTimeEpoch"))
? jobValue.getLong("runTimeEpoch") : startTimeZD.toInstant().toEpochMilli();
this.maxRetry = jobValue.get("maxRetry") != null ? jobValue.getLong("maxRetry") : 0;
Long retryCount = jobValue.getLong("currentRetryCount");
if (retryCount != null) {
Expand Down Expand Up @@ -196,7 +216,12 @@ private void createRecurrence(long next, boolean isRetryOnFailure) throws Generi
if (Debug.verboseOn()) {
Debug.logVerbose("Next runtime returned: " + next, MODULE);
}
if (next > startTime) {
/*
This solution ensures that the system uses a consistent,
UTC-based time for scheduling and rescheduling recurring jobs, even when DST changes affect the local time.
*/
ZonedDateTime nextRunTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(next), ZoneId.of("UTC"));
if (nextRunTime.toInstant().toEpochMilli() > startTime) {
String pJobId = jobValue.getString("parentJobId");
if (pJobId == null) {
pJobId = jobValue.getString("jobId");
Expand All @@ -208,7 +233,8 @@ private void createRecurrence(long next, boolean isRetryOnFailure) throws Generi
newJob.set("statusId", "SERVICE_PENDING");
newJob.set("startDateTime", null);
newJob.set("runByInstanceId", null);
newJob.set("runTime", new java.sql.Timestamp(next));
newJob.set("runTime", Timestamp.from(nextRunTime.toInstant()));
newJob.set("runTimeEpoch", nextRunTime.toInstant().toEpochMilli());
if (isRetryOnFailure) {
newJob.set("currentRetryCount", currentRetryCount + 1);
} else {
Expand Down

0 comments on commit 46701da

Please sign in to comment.