azkaban-aplcache
Changes
azkaban-common/build.gradle 1(+1 -0)
azkaban-webserver/build.gradle 4(+4 -0)
azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm 16(+13 -3)
Details
azkaban-common/build.gradle 1(+1 -0)
diff --git a/azkaban-common/build.gradle b/azkaban-common/build.gradle
index 6a24945..78c04ce 100644
--- a/azkaban-common/build.gradle
+++ b/azkaban-common/build.gradle
@@ -53,6 +53,7 @@ dependencies {
compile('org.mortbay.jetty:jetty:6.1.26')
compile('org.mortbay.jetty:jetty-util:6.1.26')
compile('org.slf4j:slf4j-api:1.6.1')
+ compile('org.quartz-scheduler:quartz:2.2.1')
testCompile(project(':azkaban-test').sourceSets.test.output)
testCompile('junit:junit:4.11')
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java b/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
index 2b2fc88..27d3f30 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
@@ -16,7 +16,13 @@
package azkaban.scheduler;
+import azkaban.executor.ExecutionOptions;
+import azkaban.sla.SlaOption;
+import azkaban.utils.Pair;
+import azkaban.utils.Utils;
+
import java.util.ArrayList;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -31,10 +37,7 @@ import org.joda.time.Months;
import org.joda.time.ReadablePeriod;
import org.joda.time.Seconds;
import org.joda.time.Weeks;
-
-import azkaban.executor.ExecutionOptions;
-import azkaban.sla.SlaOption;
-import azkaban.utils.Pair;
+import org.quartz.CronExpression;
public class Schedule {
@@ -50,6 +53,7 @@ public class Schedule {
private String submitUser;
private String status;
private long submitTime;
+ private String cronExpression;
private boolean skipPastOccurrences = true;
@@ -63,7 +67,7 @@ public class Schedule {
this(scheduleId, projectId, projectName, flowName, status, firstSchedTime,
timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser,
- null, null);
+ null, null, null);
}
public Schedule(int scheduleId, int projectId, String projectName,
@@ -74,14 +78,14 @@ public class Schedule {
this(scheduleId, projectId, projectName, flowName, status, firstSchedTime,
DateTimeZone.forID(timezoneId), parsePeriodString(period),
lastModifyTime, nextExecTime, submitTime, submitUser, executionOptions,
- slaOptions);
+ slaOptions, null);
}
public Schedule(int scheduleId, int projectId, String projectName,
String flowName, String status, long firstSchedTime,
DateTimeZone timezone, ReadablePeriod period, long lastModifyTime,
long nextExecTime, long submitTime, String submitUser,
- ExecutionOptions executionOptions, List<SlaOption> slaOptions) {
+ ExecutionOptions executionOptions, List<SlaOption> slaOptions, String cronExpression) {
this.scheduleId = scheduleId;
this.projectId = projectId;
this.projectName = projectName;
@@ -96,6 +100,7 @@ public class Schedule {
this.submitTime = submitTime;
this.executionOptions = executionOptions;
this.slaOptions = slaOptions;
+ this.cronExpression = cronExpression;
}
public ExecutionOptions getExecutionOptions() {
@@ -119,11 +124,16 @@ public class Schedule {
}
public String toString() {
- return projectName + "." + flowName + " (" + projectId + ")"
- + " to be run at (starting) "
- + new DateTime(firstSchedTime).toDateTimeISO()
- + " with recurring period of "
- + (period == null ? "non-recurring" : createPeriodString(period));
+
+ String underlying = projectName + "." + flowName + " (" + projectId + ")" + " to be run at (starting) " + new DateTime(
+ firstSchedTime).toDateTimeISO();
+ if (period == null && cronExpression == null) {
+ return underlying + " non-recurring";
+ } else if (cronExpression != null) {
+ return underlying + " with CronExpression {" + cronExpression + "}";
+ } else {
+ return underlying + " with precurring period of " + createPeriodString(period);
+ }
}
public Pair<Integer, String> getScheduleIdentityPair() {
@@ -182,11 +192,21 @@ public class Schedule {
return submitTime;
}
+ public String getCronExpression() {
+ return cronExpression;
+ }
+
public boolean updateTime() {
if (new DateTime(nextExecTime).isAfterNow()) {
return true;
}
+ if (cronExpression != null) {
+ DateTime nextTime = getNextCronRuntime(nextExecTime, timezone, Utils.parseCronExpression(cronExpression, timezone));
+ this.nextExecTime = nextTime.getMillis();
+ return true;
+ }
+
if (period != null) {
DateTime nextTime = getNextRuntime(nextExecTime, timezone, period);
@@ -224,6 +244,23 @@ public class Schedule {
return date;
}
+ /**
+ *
+ * @param scheduleTime represents the time when Schedule Servlet receives the Cron Schedule API call.
+ * @param timezone is always UTC (after 3.1.0)
+ * @param ce
+ * @return the First Scheduled DateTime to run this flow.
+ */
+ private DateTime getNextCronRuntime(long scheduleTime, DateTimeZone timezone,
+ CronExpression ce) {
+
+ Date date = new DateTime(scheduleTime).withZone(timezone).toDate();
+ if (ce != null) {
+ date = ce.getNextValidTimeAfter(date);
+ }
+ return new DateTime(date);
+ }
+
public static ReadablePeriod parsePeriodString(String periodStr) {
ReadablePeriod period;
char periodUnit = periodStr.charAt(periodStr.length() - 1);
@@ -341,7 +378,7 @@ public class Schedule {
}
public boolean isRecurring() {
- return period == null ? false : true;
+ return period != null || cronExpression != null;
}
public boolean skipPastOccurrences() {
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java b/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
index c2489ec..fbfef13 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
@@ -204,7 +204,7 @@ public class ScheduleManager implements TriggerAgent {
Schedule sched =
new Schedule(scheduleId, projectId, projectName, flowName, status,
firstSchedTime, timezone, period, lastModifyTime, nextExecTime,
- submitTime, submitUser, execOptions, slaOptions);
+ submitTime, submitUser, execOptions, slaOptions, null);
logger
.info("Scheduling flow '" + sched.getScheduleName() + "' for "
+ _dateFormat.print(firstSchedTime) + " with a period of " + period == null ? "(non-recurring)"
@@ -214,6 +214,23 @@ public class ScheduleManager implements TriggerAgent {
return sched;
}
+ public Schedule cronScheduleFlow(final int scheduleId, final int projectId,
+ final String projectName, final String flowName, final String status,
+ final long firstSchedTime, final DateTimeZone timezone,
+ final long lastModifyTime,
+ final long nextExecTime, final long submitTime, final String submitUser,
+ ExecutionOptions execOptions, List<SlaOption> slaOptions, String cronExpression) {
+ Schedule sched =
+ new Schedule(scheduleId, projectId, projectName, flowName, status,
+ firstSchedTime, timezone, null, lastModifyTime, nextExecTime,
+ submitTime, submitUser, execOptions, slaOptions, cronExpression);
+ logger
+ .info("Scheduling flow '" + sched.getScheduleName() + "' for "
+ + _dateFormat.print(firstSchedTime) + " cron Expression = " + cronExpression);
+
+ insertSchedule(sched);
+ return sched;
+ }
/**
* Schedules the flow, but doesn't save the schedule afterwards.
*
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java b/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
index 7a3c12a..e1bcf81 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
@@ -83,7 +83,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
ConditionChecker checker =
new BasicTimeChecker("BasicTimeChecker_1", s.getFirstSchedTime(),
s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
- s.getPeriod());
+ s.getPeriod(), s.getCronExpression());
checkers.put(checker.getId(), checker);
String expr = checker.getId() + ".eval()";
Condition cond = new Condition(checkers, expr);
@@ -97,7 +97,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
ConditionChecker checker =
new BasicTimeChecker("BasicTimeChecker_2", s.getFirstSchedTime(),
s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
- s.getPeriod());
+ s.getPeriod(), s.getCronExpression());
checkers.put(checker.getId(), checker);
String expr = checker.getId() + ".eval()";
Condition cond = new Condition(checkers, expr);
@@ -135,8 +135,8 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
lastUpdateTime = Math.max(lastUpdateTime, t.getLastModifyTime());
Schedule s = triggerToSchedule(t);
schedules.add(s);
- System.out.println("loaded schedule for " + s.getProjectId()
- + s.getProjectName());
+ System.out.println("loaded schedule for "
+ + s.getProjectName() + " (project_ID: " + s.getProjectId() + ")");
}
return schedules;
@@ -167,7 +167,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
t.getStatus().toString(), ck.getFirstCheckTime(),
ck.getTimeZone(), ck.getPeriod(), t.getLastModifyTime(),
ck.getNextCheckTime(), t.getSubmitTime(), t.getSubmitUser(),
- act.getExecutionOptions(), act.getSlaOptions());
+ act.getExecutionOptions(), act.getSlaOptions(), ck.getCronExpression());
return s;
} else {
logger.error("Failed to parse schedule from trigger!");
@@ -207,8 +207,8 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
lastUpdateTime = Math.max(lastUpdateTime, t.getLastModifyTime());
Schedule s = triggerToSchedule(t);
schedules.add(s);
- System.out.println("loaded schedule for " + s.getProjectId()
- + s.getProjectName());
+ System.out.println("loaded schedule for "
+ + s.getProjectName() + " (project_ID: " + s.getProjectId() + ")");
}
return schedules;
}
diff --git a/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java b/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
index 4731dde..52ff2e0 100644
--- a/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
+++ b/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
@@ -18,16 +18,22 @@ package azkaban.trigger.builtin;
import java.util.HashMap;
import java.util.Map;
+import java.util.Date;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.ReadablePeriod;
+import org.apache.log4j.Logger;
+
+import org.quartz.CronExpression;
import azkaban.trigger.ConditionChecker;
import azkaban.utils.Utils;
public class BasicTimeChecker implements ConditionChecker {
+ private static Logger logger = Logger.getLogger(BasicTimeChecker.class);
+
public static final String type = "BasicTimeChecker";
private long firstCheckTime;
@@ -37,11 +43,13 @@ public class BasicTimeChecker implements ConditionChecker {
private boolean skipPastChecks = true;
private ReadablePeriod period;
+ private String cronExpression;
+ private CronExpression cronExecutionTime;
private final String id;
public BasicTimeChecker(String id, long firstCheckTime,
DateTimeZone timezone, boolean isRecurring, boolean skipPastChecks,
- ReadablePeriod period) {
+ ReadablePeriod period, String cronExpression) {
this.id = id;
this.firstCheckTime = firstCheckTime;
this.timezone = timezone;
@@ -49,6 +57,8 @@ public class BasicTimeChecker implements ConditionChecker {
this.skipPastChecks = skipPastChecks;
this.period = period;
this.nextCheckTime = firstCheckTime;
+ this.cronExpression = cronExpression;
+ cronExecutionTime = Utils.parseCronExpression(cronExpression, timezone);
this.nextCheckTime = calculateNextCheckTime();
}
@@ -76,9 +86,13 @@ public class BasicTimeChecker implements ConditionChecker {
return nextCheckTime;
}
+ public String getCronExpression() {
+ return cronExpression;
+ }
+
public BasicTimeChecker(String id, long firstCheckTime,
DateTimeZone timezone, long nextCheckTime, boolean isRecurring,
- boolean skipPastChecks, ReadablePeriod period) {
+ boolean skipPastChecks, ReadablePeriod period, String cronExpression) {
this.id = id;
this.firstCheckTime = firstCheckTime;
this.timezone = timezone;
@@ -86,6 +100,8 @@ public class BasicTimeChecker implements ConditionChecker {
this.isRecurring = isRecurring;
this.skipPastChecks = skipPastChecks;
this.period = period;
+ this.cronExpression = cronExpression;
+ cronExecutionTime = Utils.parseCronExpression(cronExpression, timezone);
}
@Override
@@ -130,10 +146,11 @@ public class BasicTimeChecker implements ConditionChecker {
ReadablePeriod period =
Utils.parsePeriodString((String) jsonObj.get("period"));
String id = (String) jsonObj.get("id");
+ String cronExpression = (String) jsonObj.get("cronExpression");
BasicTimeChecker checker =
new BasicTimeChecker(id, firstCheckTime, timezone, nextCheckTime,
- isRecurring, skipPastChecks, period);
+ isRecurring, skipPastChecks, period, cronExpression);
if (skipPastChecks) {
checker.updateNextCheckTime();
}
@@ -157,8 +174,11 @@ public class BasicTimeChecker implements ConditionChecker {
throw new IllegalStateException(
"100000 increments of period did not get to present time.");
}
- if (period == null) {
+ if (period == null && cronExpression == null) {
break;
+ } else if (cronExecutionTime != null) {
+ Date nextDate = cronExecutionTime.getNextValidTimeAfter(date.toDate());
+ date = new DateTime(nextDate);
} else {
date = date.plus(period);
}
@@ -186,6 +206,7 @@ public class BasicTimeChecker implements ConditionChecker {
jsonObj.put("skipPastChecks", String.valueOf(skipPastChecks));
jsonObj.put("period", Utils.createPeriodString(period));
jsonObj.put("id", id);
+ jsonObj.put("cronExpression", cronExpression);
return jsonObj;
}
diff --git a/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java b/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
index e825202..89f4622 100644
--- a/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
+++ b/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
@@ -60,7 +60,7 @@ public class TriggerManager extends EventHandler implements
private final Object syncObj = new Object();
private String scannerStage = "";
-
+
public TriggerManager(Props props, TriggerLoader triggerLoader,
ExecutorManager executorManager) throws TriggerManagerException {
@@ -277,6 +277,7 @@ public class TriggerManager extends EventHandler implements
shouldSkip = false;
}
+ logger.info("Get Next Check Time =" + t.getNextCheckTime() + " now = " + now );
if (shouldSkip) {
logger.info("Skipping trigger" + t.getTriggerId() + " until " + t.getNextCheckTime());
}
diff --git a/azkaban-common/src/main/java/azkaban/utils/Utils.java b/azkaban-common/src/main/java/azkaban/utils/Utils.java
index 9cdeaaf..ca53c76 100644
--- a/azkaban-common/src/main/java/azkaban/utils/Utils.java
+++ b/azkaban-common/src/main/java/azkaban/utils/Utils.java
@@ -30,11 +30,17 @@ import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Random;
+import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
+import java.text.ParseException;
import org.apache.commons.io.IOUtils;
+
+import org.apache.log4j.Logger;
+
+import org.joda.time.DateTimeZone;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.DurationFieldType;
@@ -46,10 +52,15 @@ import org.joda.time.Seconds;
import org.joda.time.Weeks;
import org.joda.time.Years;
+import org.quartz.CronExpression;
+
/**
* A util helper class full of static methods that are commonly used.
*/
public class Utils {
+
+ private static Logger logger = Logger
+ .getLogger(Utils.class);
public static final Random RANDOM = new Random();
/**
@@ -455,4 +466,30 @@ public class Utils {
return sizeInKb;
}
+
+ /**
+ * @param cronExpression: A cron expression is a string separated by white space, to provide a parser and evaluator for Quartz cron expressions.
+ * @return : org.quartz.CronExpression object.
+ *
+ * TODO: Currently, we have to transform Joda Timezone to Java Timezone due to CronExpression.
+ * Since Java8 enhanced Time functionalities, We consider transform all Jodatime to Java Time in future.
+ *
+ */
+ public static CronExpression parseCronExpression(String cronExpression, DateTimeZone timezone) {
+ if (cronExpression != null) {
+ try {
+ CronExpression ce = new CronExpression(cronExpression);
+ ce.setTimeZone(TimeZone.getTimeZone(timezone.getID()));
+ return ce;
+ } catch (ParseException pe) {
+ logger.error("this cron expression {" + cronExpression + "} can not be parsed. "
+ + "Please Check Quartz Cron Syntax.");
+ }
+ return null;
+ } else return null;
+ }
+
+ public static boolean isCronExpressionValid(String cronExpression) {
+ return CronExpression.isValidExpression(cronExpression);
+ }
}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
index 926f705..38b0f4d 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
@@ -20,6 +20,7 @@ import java.util.HashMap;
import java.util.Map;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.ReadablePeriod;
import org.junit.Test;
@@ -32,11 +33,18 @@ import azkaban.trigger.builtin.BasicTimeChecker;
public class BasicTimeCheckerTest {
- @Test
- public void basicTimerTest() {
+ private Condition getCondition(BasicTimeChecker timeChecker){
Map<String, ConditionChecker> checkers =
new HashMap<String, ConditionChecker>();
+ checkers.put(timeChecker.getId(), timeChecker);
+ String expr = timeChecker.getId() + ".eval()";
+
+ return new Condition(checkers, expr);
+ }
+
+ @Test
+ public void periodTimerTest() {
// get a new timechecker, start from now, repeat every minute. should
// evaluate to false now, and true a minute later.
@@ -45,12 +53,9 @@ public class BasicTimeCheckerTest {
BasicTimeChecker timeChecker =
new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
- now.getZone(), true, true, period);
- checkers.put(timeChecker.getId(), timeChecker);
- String expr = timeChecker.getId() + ".eval()";
+ now.getZone(), true, true, period, null);
- Condition cond = new Condition(checkers, expr);
- System.out.println(expr);
+ Condition cond = getCondition(timeChecker);
assertFalse(cond.isMet());
@@ -63,9 +68,7 @@ public class BasicTimeCheckerTest {
}
assertTrue(cond.isMet());
-
cond.resetCheckers();
-
assertFalse(cond.isMet());
// sleep for 1 min
@@ -77,6 +80,191 @@ public class BasicTimeCheckerTest {
}
assertTrue(cond.isMet());
+ }
+
+ /**
+ * Test Base Cron Functionality.
+ */
+ @Test
+ public void testQuartzCurrentZone() {
+
+ DateTime now = DateTime.now();
+ String cronExpression = "0 0 0 31 12 ? 2050";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ now.getZone(), true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+ // 2556086400000L represent for "2050-12-31T00:00:00.000-08:00"
+
+ DateTime year2050 = new DateTime(2050, 12, 31, 0 ,0 ,0 ,now.getZone());
+ assertTrue(cond.getNextCheckTime() == year2050.getMillis());
+ }
+
+ /**
+ * Test when PST-->PDT happens in 2020. -8:00 -> -7:00
+ * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+ *
+ * This test demonstrates that if the cron is under UTC settings,
+ * When daylight saving change occurs, 2:30 will be changed to 3:30 at that day.
+ */
+ @Test
+ public void testPSTtoPDTunderUTC() {
+
+ DateTime now = DateTime.now();
+
+ // 10:30 UTC == 2:30 PST
+ String cronExpression = "0 30 10 8 3 ? 2020";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ DateTimeZone.UTC, true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+
+ DateTime spring2020UTC = new DateTime(2020, 3, 8, 10, 30, 0, DateTimeZone.UTC);
+ DateTime spring2020PDT = new DateTime(2020, 3, 8, 3, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+ assertTrue(cond.getNextCheckTime() == spring2020UTC.getMillis());
+ assertTrue(cond.getNextCheckTime() == spring2020PDT.getMillis());
+ }
+
+ /**
+ * Test when PST-->PDT happens in 2020. -8:00 -> -7:00
+ * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+ *
+ * This test demonstrates that 2:30 AM will not happen during the daylight saving day on Cron settings under PDT/PST.
+ * Since we let the cron triggered both at March 8th, and 9th, it will execute at March 9th.
+ */
+ @Test
+ public void testPSTtoPDTdst2() {
+
+ DateTime now = DateTime.now();
+
+ String cronExpression = "0 30 2 8,9 3 ? 2020";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecker_1", now.getMillis(),
+ DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+
+ DateTime aTime = new DateTime(2020, 3, 9, 2, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+ assertTrue(cond.getNextCheckTime() == aTime.getMillis());
+ }
+
+ /**
+ * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+ * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+ *
+ * This test cronDayLightPacificWinter1 is in order to compare against the cronDayLightPacificWinter2.
+ *
+ * In this Test, we let job run at 1:00 at Nov.1st, 2020. We know that we will have two 1:00 at that day.
+ * The test shows that the first 1:00 is skipped at that day.
+ * Schedule will still be executed once on that day.
+ */
+ @Test
+ public void testPDTtoPSTdst1() {
+
+ DateTime now = DateTime.now();
+
+ // 9:00 UTC == 1:00 PST (difference is 8 hours)
+ String cronExpression = "0 0 1 1,2 11 ? 2020";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+
+ DateTime winter2020 = new DateTime(2020, 11, 1, 9, 0, 0, DateTimeZone.UTC);
+
+ DateTime winter2020_2 = new DateTime(2020, 11, 1, 1, 0, 0, DateTimeZone.forID("America/Los_Angeles"));
+ DateTime winter2020_3 = new DateTime(2020, 11, 1, 2, 0, 0, DateTimeZone.forID("America/Los_Angeles"));
+ assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
+
+
+ // Both 1 and 2 o'clock can not pass the test. Based on milliseconds we got,
+ // winter2020_2.getMillis() == 11/1/2020, 1:00:00 AM GMT-7:00 DST
+ // winter2020_3.getMillis() == 11/1/2020, 2:00:00 AM GMT-8:00
+ // Both time doesn't match the second 1:00 AM
+ assertFalse(cond.getNextCheckTime() == winter2020_2.getMillis());
+ assertFalse(cond.getNextCheckTime() == winter2020_3.getMillis());
+ }
+
+
+ /**
+ * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+ * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+ *
+ * This test cronDayLightPacificWinter2 is in order to be compared against the cronDayLightPacificWinter1.
+ *
+ * In this Test, we let job run at 0:59 at Nov.1st, 2020. it shows that it is 7:59 UTC
+ * The test shows 7:59 UTC jump to 9:00 UTC.
+ */
+ @Test
+ public void testPDTtoPSTdst2() {
+
+ DateTime now = DateTime.now();
+
+ // 7:59 UTC == 0:59 PDT (difference is 7 hours)
+ String cronExpression = "0 59 0 1,2 11 ? 2020";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+
+ // 7:59 UTC == 0:59 PDT (difference is 7 hours)
+ DateTime winter2020 = new DateTime(2020, 11, 1, 7, 59, 0, DateTimeZone.UTC);
+ DateTime winter2020_2 = new DateTime(2020, 11, 1, 0, 59, 0, DateTimeZone.forID("America/Los_Angeles"));
+
+ // Local time remains the same.
+ assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
+ assertTrue(cond.getNextCheckTime() == winter2020_2.getMillis());
+ }
+
+
+
+ /**
+ * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+ * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+ *
+ * This test is a supplement to cronDayLightPacificWinter1.
+ *
+ * Still, we let job run at 1:30 at Nov.1st, 2020. We know that we will have two 1:30 at that day.
+ * The test shows the 1:30 at that day will be based on PST, not PDT. It means that the first 1:30 is skipped at that day.
+ */
+ @Test
+ public void testPDTtoPSTdst3() {
+
+ DateTime now = DateTime.now();
+
+ // 9:30 UTC == 1:30 PST (difference is 8 hours)
+ String cronExpression = "0 30 1 1,2 11 ? 2020";
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+ System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+ Condition cond = getCondition(timeChecker);
+
+ // 9:30 UTC == 1:30 PST (difference is 8 hours)
+ DateTime winter2020 = new DateTime(2020, 11, 1, 9, 30, 0, DateTimeZone.UTC);
+
+ DateTime winter2020_2 = new DateTime(2020, 11, 1, 1, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+ DateTime winter2020_3 = new DateTime(2020, 11, 1, 2, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+ assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
+ // Both 1:30 and 2:30 can not pass the test.
+ assertFalse(cond.getNextCheckTime() == winter2020_2.getMillis());
+ assertFalse(cond.getNextCheckTime() == winter2020_3.getMillis());
}
}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
index cfff784..cf5a1fa 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
@@ -85,7 +85,7 @@ public class ConditionTest {
// period);
ConditionChecker timeChecker =
new BasicTimeChecker("BasicTimeChecker_1", now.getMillis(),
- now.getZone(), true, true, Utils.parsePeriodString(period));
+ now.getZone(), true, true, Utils.parsePeriodString(period), null);
System.out.println("checker id is " + timeChecker.getId());
checkers.put(timeChecker.getId(), timeChecker);
diff --git a/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
index 01fbb29..b55a2ba 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
@@ -206,7 +206,7 @@ public class JdbcTriggerLoaderTest {
DateTime now = DateTime.now();
ConditionChecker checker1 =
new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
- true, true, Utils.parsePeriodString("1h"));
+ true, true, Utils.parsePeriodString("1h"), null);
Map<String, ConditionChecker> checkers1 =
new HashMap<String, ConditionChecker>();
checkers1.put(checker1.getId(), checker1);
diff --git a/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
index a0e1f51..595461f 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
@@ -57,7 +57,7 @@ public class TriggerTest {
DateTime now = DateTime.now();
ConditionChecker checker1 =
new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
- true, true, Utils.parsePeriodString("1h"));
+ true, true, Utils.parsePeriodString("1h"), null);
Map<String, ConditionChecker> checkers1 =
new HashMap<String, ConditionChecker>();
checkers1.put(checker1.getId(), checker1);
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
index 85260d5..013b410 100644
--- a/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
@@ -44,9 +44,9 @@ public class BlockingStatusTest {
}
/**
- * TODO: Ignore this test at present since travis in Github can not always pass this test.
- * We will modify the below code to make travis pass in future.
- */
+ * TODO: Ignore this test at present since travis in Github can not always pass this test.
+ * We will modify the below code to make travis pass in future.
+ */
@Ignore @Test
public void testFinishedBlock() {
BlockingStatus status = new BlockingStatus(1, "test", Status.SKIPPED);
diff --git a/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java b/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
index b2a7e1e..079fe04 100644
--- a/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
+++ b/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
@@ -210,6 +210,7 @@ public class Schedule2Trigger {
DateTimeZone timezone = DateTimeZone.forID(timezoneId);
ReadablePeriod period =
Utils.parsePeriodString(schedProps.getString("period"));
+ String cronExpression = schedProps.getString("cronExpression");
// DateTime lastModifyTime = DateTime.now();
long nextExecTimeLong = schedProps.getLong("nextExecTimeLong");
// DateTime nextExecTime = new DateTime(nextExecTimeLong);
@@ -246,7 +247,7 @@ public class Schedule2Trigger {
new azkaban.scheduler.Schedule(-1, projectId, projectName,
flowName, "ready", firstSchedTimeLong, timezone, period,
DateTime.now().getMillis(), nextExecTimeLong, submitTimeLong,
- submitUser, executionOptions, slaOptions);
+ submitUser, executionOptions, slaOptions, cronExpression);
Trigger t = scheduleToTrigger(schedule);
logger.info("Ready to insert trigger " + t.getDescription());
triggerLoader.addTrigger(t);
@@ -290,7 +291,7 @@ public class Schedule2Trigger {
ConditionChecker checker =
new BasicTimeChecker("BasicTimeChecker_1", s.getFirstSchedTime(),
s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
- s.getPeriod());
+ s.getPeriod(), s.getCronExpression());
checkers.put(checker.getId(), checker);
String expr = checker.getId() + ".eval()";
Condition cond = new Condition(checkers, expr);
@@ -305,7 +306,7 @@ public class Schedule2Trigger {
ConditionChecker checker =
new BasicTimeChecker("BasicTimeChecker_2", s.getFirstSchedTime(),
s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
- s.getPeriod());
+ s.getPeriod(),s.getCronExpression());
checkers.put(checker.getId(), checker);
String expr = checker.getId() + ".eval()";
Condition cond = new Condition(checkers, expr);
azkaban-webserver/build.gradle 4(+4 -0)
diff --git a/azkaban-webserver/build.gradle b/azkaban-webserver/build.gradle
index 7440c4a..2801f9b 100644
--- a/azkaban-webserver/build.gradle
+++ b/azkaban-webserver/build.gradle
@@ -17,6 +17,10 @@ bower {
source 'build/js/*.min.js' >> '/'
excludes 'jquery'
}
+
+ 'later'('1.2.0'){
+ source 'later.min.js' >> '/'
+ }
}
apply plugin: 'lesscss'
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index 12c5993..b19021d 100644
--- a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.TimeZone;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -30,8 +31,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
import azkaban.server.AzkabanServer;
import azkaban.server.HttpRequestUtils;
@@ -48,8 +47,6 @@ import azkaban.webapp.plugin.TriggerPlugin;
* Base Servlet for pages
*/
public abstract class AbstractAzkabanServlet extends HttpServlet {
- private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat
- .forPattern("z");
private static final String AZKABAN_SUCCESS_MESSAGE =
"azkaban.success.message";
private static final String AZKABAN_WARN_MESSAGE =
@@ -72,6 +69,12 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
private String label;
private String color;
+ /*
+ * The variable schedulePanelPageName is in charge of switching on retired schedulePanelDeprecated.vm (old UI)
+ * or the new schedulePanel.vm (new UI). We can configure it in conf for this binary change.
+ */
+ private String schedulePanelPageName;
+
private List<ViewerPlugin> viewerPlugins;
private List<TriggerPlugin> triggerPlugins;
@@ -99,6 +102,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
name = props.getString("azkaban.name", "");
label = props.getString("azkaban.label", "");
color = props.getString("azkaban.color", "#FF0000");
+ schedulePanelPageName = props.getString("azkaban.schedulePanelPageName", "schedulepanelDeprecated.vm");
if (application instanceof AzkabanWebServer) {
AzkabanWebServer server = (AzkabanWebServer) application;
@@ -330,8 +334,9 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
page.add("azkaban_name", name);
page.add("azkaban_label", label);
page.add("azkaban_color", color);
+ page.add("switchToPanelPage", schedulePanelPageName);
page.add("utils", utils);
- page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+ page.add("timezone", TimeZone.getDefault().getID());
page.add("currentTime", (new DateTime()).getMillis());
if (session != null && session.getUser() != null) {
page.add("user_id", session.getUser().getUserId());
@@ -380,7 +385,8 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
page.add("azkaban_name", name);
page.add("azkaban_label", label);
page.add("azkaban_color", color);
- page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+ page.add("switchToPanelPage", schedulePanelPageName);
+ page.add("timezone", TimeZone.getDefault().getID());
page.add("currentTime", (new DateTime()).getMillis());
page.add("context", req.getContextPath());
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
index a36505b..14d5b89 100644
--- a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
@@ -114,6 +114,8 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
ret = null;
} else if (ajaxName.equals("scheduleFlow")) {
ajaxScheduleFlow(req, ret, session.getUser());
+ } else if (ajaxName.equals("scheduleCronFlow")) {
+ ajaxScheduleCronFlow(req, ret, session.getUser());
} else if (ajaxName.equals("fetchSchedule")) {
ajaxFetchSchedule(req, ret, session.getUser());
}
@@ -249,6 +251,7 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
jsonObj.put("nextExecTime",
utils.formatDateTime(schedule.getNextExecTime()));
jsonObj.put("period", utils.formatPeriod(schedule.getPeriod()));
+ jsonObj.put("cronExpression", schedule.getCronExpression());
jsonObj.put("executionOptions", schedule.getExecutionOptions());
ret.put("schedule", jsonObj);
}
@@ -384,6 +387,8 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
String action = getParam(req, "action");
if (action.equals("scheduleFlow")) {
ajaxScheduleFlow(req, ret, session.getUser());
+ } else if (action.equals("scheduleCronFlow")) {
+ ajaxScheduleCronFlow(req, ret, session.getUser());
} else if (action.equals("removeSched")) {
ajaxRemoveSched(req, ret, session.getUser());
}
@@ -684,6 +689,87 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
ret.put("message", projectName + "." + flowName + " scheduled.");
}
+ /**
+ *
+ * This method is in charge of doing cron scheduling.
+ * @throws ServletException
+ */
+ private void ajaxScheduleCronFlow(HttpServletRequest req,
+ HashMap<String, Object> ret, User user) throws ServletException {
+ String projectName = getParam(req, "projectName");
+ String flowName = getParam(req, "flow");
+
+ Project project = projectManager.getProject(projectName);
+
+ if (project == null) {
+ ret.put("message", "Project " + projectName + " does not exist");
+ ret.put("status", "error");
+ return;
+ }
+ int projectId = project.getId();
+
+ if (!hasPermission(project, user, Type.SCHEDULE)) {
+ ret.put("status", "error");
+ ret.put("message", "Permission denied. Cannot execute " + flowName);
+ return;
+ }
+
+ Flow flow = project.getFlow(flowName);
+ if (flow == null) {
+ ret.put("status", "error");
+ ret.put("message", "Flow " + flowName + " cannot be found in project "
+ + project);
+ return;
+ }
+
+ DateTimeZone timezone = DateTimeZone.getDefault();
+ DateTime firstSchedTime = getPresentTimeByTimezone(timezone);
+
+ String cronExpression = null;
+ try {
+ if (hasParam(req, "cronExpression")) {
+ // everything in Azkaban functions is at the minute granularity, so we add 0 here
+ // to let the expression to be complete.
+ cronExpression = getParam(req, "cronExpression");
+ if(azkaban.utils.Utils.isCronExpressionValid(cronExpression) == false) {
+ ret.put("error", "This expression <" + cronExpression + "> can not be parsed to quartz cron.");
+ return;
+ }
+ }
+ if(cronExpression == null)
+ throw new Exception("Cron expression must exist.");
+ } catch (Exception e) {
+ ret.put("error", e.getMessage());
+ }
+
+ ExecutionOptions flowOptions = null;
+ try {
+ flowOptions = HttpRequestUtils.parseFlowOptions(req);
+ HttpRequestUtils.filterAdminOnlyFlowParams(userManager, flowOptions, user);
+ } catch (Exception e) {
+ ret.put("error", e.getMessage());
+ }
+
+ List<SlaOption> slaOptions = null;
+
+ // Because either cronExpression or recurrence exists, we build schedule in the below way.
+ Schedule schedule = scheduleManager.cronScheduleFlow(-1, projectId, projectName, flowName,
+ "ready", firstSchedTime.getMillis(), firstSchedTime.getZone(),
+ DateTime.now().getMillis(), firstSchedTime.getMillis(),
+ firstSchedTime.getMillis(), user.getUserId(), flowOptions,
+ slaOptions, cronExpression);
+
+ logger.info("User '" + user.getUserId() + "' has scheduled " + "["
+ + projectName + flowName + " (" + projectId + ")" + "].");
+ projectManager.postProjectEvent(project, EventType.SCHEDULE,
+ user.getUserId(), "Schedule " + schedule.toString()
+ + " has been added.");
+
+ ret.put("status", "success");
+ ret.put("scheduleId", schedule.getScheduleId());
+ ret.put("message", projectName + "." + flowName + " scheduled.");
+ }
+
private DateTime parseDateTime(String scheduleDate, String scheduleTime) {
// scheduleTime: 12,00,pm,PDT
String[] parts = scheduleTime.split(",", -1);
@@ -713,4 +799,19 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
return firstSchedTime;
}
+
+ /**
+ * @param cronTimezone represents the timezone from remote API call
+ * @return if the string is equal to UTC, we return UTC; otherwise, we always return default timezone.
+ */
+ private DateTimeZone parseTimeZone(String cronTimezone) {
+ if(cronTimezone != null && cronTimezone.equals("UTC"))
+ return DateTimeZone.UTC;
+
+ return DateTimeZone.getDefault();
+ }
+
+ private DateTime getPresentTimeByTimezone(DateTimeZone timezone) {
+ return new DateTime(timezone);
+ }
}
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index f526573..99c7541 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -201,7 +201,7 @@
</div><!-- /modal -->
#if (!$show_schedule || $show_schedule == 'true')
- #parse ("azkaban/webapp/servlet/velocity/schedulepanel.vm")
+ #parse ("azkaban/webapp/servlet/velocity/${switchToPanelPage}")
#end
#*
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
index 6657fb1..663738d 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
@@ -36,7 +36,7 @@
var timezone = "${timezone}";
var errorMessage = null;
var successMessage = null;
-
+
$(document).ready(function () {
var flowTable = $("#scheduledFlowsTbl");
flowTable.tablesorter();
@@ -80,6 +80,7 @@
<th class="date">First Scheduled to Run</th>
<th class="date">Next Execution Time</th>
<th class="date">Repeats Every</th>
+ <th class="date">Cron Expression</th>
<th>Has SLA</th>
<th colspan="2" class="action ignoresort">Action</th>
</tr>
@@ -101,7 +102,16 @@
<td>${sched.submitUser}</td>
<td>$utils.formatDateTime(${sched.firstSchedTime})</td>
<td>$utils.formatDateTime(${sched.nextExecTime})</td>
- <td>$utils.formatPeriod(${sched.period})</td>
+ #if (${sched.period})
+ <td> $utils.formatPeriod(${sched.period}) </td>
+ #else
+ <td>Not Applicable</td>
+ #end
+ #if (${sched.cronExpression})
+ <td> ${sched.cronExpression} </td>
+ #else
+ <td>Not Applicable</td>
+ #end
<td>#if(${sched.slaOptions}) true #else false #end</td>
<td><button type="button" id="removeSchedBtn" class="btn btn-sm btn-danger" onclick="removeSched(${sched.scheduleId})" >Remove Schedule</button></td>
<td><button type="button" id="addSlaBtn" class="btn btn-sm btn-primary" onclick="slaView.initFromSched(${sched.scheduleId}, '${sched.flowName}')" >Set SLA</button></td>
@@ -109,7 +119,7 @@
#end
#else
<tr>
- <td colspan="10">No scheduled flow found.</td>
+ <td colspan="11">No scheduled flow found.</td>
</tr>
#end
</tbody>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
index 8aff70f..3f373eb 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
@@ -14,63 +14,116 @@
* the License.
*#
- <script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
- <script type="text/javascript" src="${context}/js/azkaban/view/schedule-panel.js"></script>
+<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+<script type="text/javascript" src="${context}/js/azkaban/view/schedule-panel.js"></script>
+<script type="text/javascript" src="${context}/js/moment.min.js" ></script>
+<script type="text/javascript" src="${context}/js/later.min.js"></script>
+<script type="text/javascript" src="${context}/js/moment-timezone-with-data-2010-2020.min.js"></script>
+<style type="text/css">
+ .input-box { position: relative; }
+ .input-box input { display: block; border: 1px solid #d7d6d6; background: #fff; padding: 10px 10px 10px 20px; width: 195px; }
+ .unit { position: absolute; display: block; left: 5px; top: 7px; z-index: 9; }
+</style>
+<div class="modal" id="schedule-modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Schedule Flow Options</h4>
+ </div><!-- /modal-header -->
+ <div class="modal-body">
+ <fieldset class="form-horizontal">
+ <div class="form-group">
- <div class="modal" id="schedule-modal">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h4 class="modal-title">Schedule Flow Options</h4>
- </div><!-- /modal-header -->
- <div class="modal-body">
- <fieldset class="form-horizontal">
- <div class="form-group">
- <label class="col-sm-2 control-label">Time</label>
- <div class="col-sm-7">
- <input type="text" id="timepicker" class="form-control">
- </div>
- <div class="col-sm-3">
- <select id="timezone" class="form-control">
- <option>${timezone}</option>
- <option>UTC</option>
- </select>
- </div>
+ <div class="col-sm-12" style="height:55px;">
+ <h4 style="color:Coral; font-style:italic;">All schedules are basead on the server timezone: <b id="timeZoneID"></b>.</h4>
+
+ </div>
+ <div class="form-group">
+ <div class="col-sm-6">
+ <label class="col-sm-3 control-label" id="min_label">Min</label>
+ <div class="col-sm-9">
+ <input type="text" id="minute_input" value="0" class="form-control" oninput="updateOutput()">
+ </div>
+ <br/> <br/> <br/>
+ <label class="col-sm-3 control-label" id="hour_label">Hours</label>
+ <div class="col-sm-9">
+ <input type="text" id="hour_input" value="5,7-10" class="form-control"
+ oninput="updateOutput()">
</div>
- <div class="form-group">
- <label class="col-sm-2 control-label">Date</label>
- <div class="col-sm-10">
- <input type="text" id="datepicker" class="form-control">
- </div>
+ <br/> <br/> <br/>
+ <label class="col-sm-3 control-label" id="dom_label">DOM</label>
+ <div class="col-sm-9">
+ <input type="text" id="dom_input" value="?" class="form-control"
+ oninput="updateOutput()">
</div>
- <div class="form-group">
- <label class="col-sm-2">Recurrence</label>
- <div class="col-sm-3">
- <div class="checkbox">
- <input id="is_recurring" type="checkbox" checked="checked">
- <label>repeat every</label>
- </div>
- </div>
- <div class="col-sm-2">
- <input id="period" type="text" size="2" value="1" class="form-control">
- </div>
- <div class="col-sm-3">
- <select id="period_units" class="form-control">
- <option value="d">Days</option>
- <option value="h">Hours</option>
- <option value="m">Minutes</option>
- <option value="M">Months</option>
- <option value="w">Weeks</option>
- </select>
- </div>
+ <br/> <br/> <br/>
+ <label class="col-sm-3 control-label" id="mon_label">Month</label>
+ <div class="col-sm-9">
+ <input type="text" id="month_input" value="*" class="form-control"
+ oninput="updateOutput()">
</div>
- </fieldset>
+ <br/> <br/> <br/>
+ <label class="col-sm-3 control-label" id="dow_label">DOW</label>
+ <div class="col-sm-9">
+ <input type="text" id="dow_input" value="4-6" class="form-control"
+ oninput="updateOutput()">
+ </div>
+ </div>
+
+ <div class="col-sm-5" style="background-color:#f5f5f5; border:1px solid #e3e3e3">
+ <h4 style="color:orange">Special Characters:</h4>
+ <table class="table table-striped" data-row-style="rowColors" id="instructions">
+ <tbody>
+ <tr class="success">
+ <th scope="row">*</th>
+ <td>any value</td>
+ </tr>
+ <tr class="primary">
+ <th scope="row">,</th>
+ <td>value list separators</td>
+ </tr>
+ <tr class="warning">
+ <th scope="row">-</th>
+ <td>range of values</td>
+ </tr>
+ <tr class="danger">
+ <th scope="row">/</th>
+ <td>step values</td>
+ </tr>
+ </tbody>
+ </table>
+ <p><u><span style="color:Indigo"><a href="http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html">Detailed instructions</a></span>.</u></p>
+ </div>
</div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
- <button type="button" class="btn btn-success" id="schedule-button">Schedule</button>
+
+ <div class="col-sm-offset-1 col-sm-4">
+ <div class="input-box">
+ <input type="text" id="cron-output" value="0 5,7-10 ? * 4-6" oninput = "updateExpression()" class="form-control">
+ <span class="unit">0</span>
+ </div>
</div>
- </div>
- </div>
+ <button type="button" class="col-sm-offset-1 btn btn-warning" id="clearCron">Reset</button>
+ <div class="col-xs-12" style="height:20px;"></div>
+
+ <div class="col-sm-12" style="background-color:#f5f5f5; border:1px solid #e3e3e3">
+ <h4>
+ Next 10 scheduled executions:
+ </h4>
+ <ul id="nextRecurId">
+ </ul>
+ </div>
+
+ <h3 id="cronTranslate" style="color:DeepSkyBlue; font-style:italic;"></h3>
+ <div class="col-sm-9 col-sm-offset-3">
+ <p class="text-right" id="translationWarning"> </p>
+ </div>
+ </fieldset>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-success" id="schedule-button">Schedule</button>
</div>
+ </div>
+ </div>
+</div>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanelDeprecated.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanelDeprecated.vm
new file mode 100644
index 0000000..48df207
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanelDeprecated.vm
@@ -0,0 +1,76 @@
+#*
+ * Copyright 2012 LinkedIn Corp.
+ *
+ * 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.
+*#
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/schedule-panelDeprecated.js"></script>
+
+ <div class="modal" id="schedule-modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Schedule Flow Options</h4>
+ </div><!-- /modal-header -->
+ <div class="modal-body">
+ <fieldset class="form-horizontal">
+ <div class="form-group">
+ <label class="col-sm-2 control-label">Time</label>
+ <div class="col-sm-7">
+ <input type="text" id="timepicker" class="form-control">
+ </div>
+ <div class="col-sm-3">
+ <select id="timezone" class="form-control">
+ <option>${timezone}</option>
+ <option>UTC</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label">Date</label>
+ <div class="col-sm-10">
+ <input type="text" id="datepicker" class="form-control">
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2">Recurrence</label>
+ <div class="col-sm-3">
+ <div class="checkbox">
+ <input id="is_recurring" type="checkbox" checked="checked">
+ <label>repeat every</label>
+ </div>
+ </div>
+ <div class="col-sm-2">
+ <input id="period" type="text" size="2" value="1" class="form-control">
+ </div>
+ <div class="col-sm-3">
+ <select id="period_units" class="form-control">
+ <option value="d">Days</option>
+ <option value="h">Hours</option>
+ <option value="m">Minutes</option>
+ <option value="M">Months</option>
+ <option value="w">Weeks</option>
+ </select>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-success" id="schedule-button">Schedule</button>
+ </div>
+ </div>
+ </div>
+ </div>
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
index 249f1e4..803622d 100644
--- a/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
@@ -23,8 +23,6 @@ azkaban.SchedulePanelView = Backbone.View.extend({
},
initialize: function(settings) {
- $("#timepicker").datetimepicker({format: 'LT'});
- $("#datepicker").datetimepicker({format: 'L'});
},
render: function() {
@@ -39,40 +37,30 @@ azkaban.SchedulePanelView = Backbone.View.extend({
},
scheduleFlow: function() {
- var timeVal = $('#timepicker').val();
- var timezoneVal = $('#timezone').val();
-
- var dateVal = $('#datepicker').val();
-
- var is_recurringVal = $('#is_recurring').val();
- var periodVal = $('#period').val();
- var periodUnits = $('#period_units').val();
-
var scheduleURL = contextURL + "/schedule"
var scheduleData = flowExecuteDialogView.getExecutionOptionData();
- console.log("Creating schedule for " + projectName + "." +
- scheduleData.flow);
+ console.log("Creating schedule for " + projectName + "." + scheduleData.flow);
- var scheduleTime = moment(timeVal, 'h/mm A').format('h,mm,A,') + timezoneVal;
- console.log(scheduleTime);
+ var currentMomentTime = moment();
+ var scheduleTime = currentMomentTime.utc().format('h,mm,A,')+"UTC";
+ var scheduleDate = currentMomentTime.format('MM/DD/YYYY');
- var scheduleDate = $('#datepicker').val();
- var is_recurring = document.getElementById('is_recurring').checked
- ? 'on' : 'off';
- var period = $('#period').val() + $('#period_units').val();
-
- scheduleData.ajax = "scheduleFlow";
+ scheduleData.ajax = "scheduleCronFlow";
scheduleData.projectName = projectName;
- scheduleData.scheduleTime = scheduleTime;
- scheduleData.scheduleDate = scheduleDate;
- scheduleData.is_recurring = is_recurring;
- scheduleData.period = period;
+ scheduleData.cronExpression = "0 " + $('#cron-output').val();
+
+ // Currently, All cron expression will be based on server timezone.
+ // Later we might implement a feature support cron under various timezones, depending on the future use cases.
+ // scheduleData.cronTimezone = timezone;
+
+ console.log("current Time = " + scheduleDate + " " + scheduleTime );
+ console.log("cronExpression = " + scheduleData.cronExpression);
var successHandler = function(data) {
if (data.error) {
schedulePanelView.hideSchedulePanel();
- messageDialogView.show("Error Scheduling Flow", data.message);
+ messageDialogView.show("Error Scheduling Flow", data.error);
}
else {
schedulePanelView.hideSchedulePanel();
@@ -90,4 +78,163 @@ $(function() {
schedulePanelView = new azkaban.SchedulePanelView({
el: $('#schedule-modal')
});
+
+ // To compute the current timezone's time offset against UTC.
+ // Currently not useful.
+ // var TimeZoneOffset = new Date().toString().match(/([-\+][0-9]+)\s/)[1];
+
+ $('#timeZoneID').html(timezone);
+
+ updateOutput();
+ $("#clearCron").click(function () {
+ $('#cron-output').val("* * * * ?");
+ resetLabelColor();
+ $("#minute_input").val("*");
+ $("#hour_input").val("*");
+ $("#dom_input").val("*");
+ $("#month_input").val("*");
+ $("#dow_input").val("?");
+ $(cron_translate_id).text("")
+ $(cron_translate_warning_id).text("")
+ $('#nextRecurId').html("");
+
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ });
+
+ $("#minute_input").click(function () {
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ resetLabelColor();
+ $("#min_label").css("color", "red");
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("0-59");
+ $('#instructions tbody tr:last td').html("allowed values");
+ });
+
+ $("#hour_input").click(function () {
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ resetLabelColor();
+ $("#hour_label").css("color", "red");
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("0-23");
+ $('#instructions tbody tr:last td').html("allowed values");
+ });
+
+ $("#dom_input").click(function () {
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ resetLabelColor();
+ $("#dom_label").css("color", "red");
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("1-31");
+ $('#instructions tbody tr:last td').html("allowed values");
+
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last').find('td').css({'class': 'danger'});
+ $('#instructions tbody tr:last th').html("?");
+ $('#instructions tbody tr:last td').html("Blank");
+ });
+
+ $("#month_input").click(function () {
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ resetLabelColor();
+ $("#mon_label").css("color", "red");
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("1-12");
+ $('#instructions tbody tr:last td').html("allowed values");
+ });
+
+ $("#dow_input").click(function () {
+ while ($("#instructions tbody tr:last").index() >= 4) {
+ $("#instructions tbody tr:last").remove();
+ }
+ resetLabelColor();
+ $("#dow_label").css("color", "red");
+
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("1-7");
+ $('#instructions tbody tr:last td').html("SUN MON TUE WED THU FRI SAT");
+
+ $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+ $('#instructions tbody tr:last th').html("?");
+ $('#instructions tbody tr:last td').html("Blank");
+ });
});
+
+function resetLabelColor(){
+ $("#min_label").css("color", "black");
+ $("#hour_label").css("color", "black");
+ $("#dom_label").css("color", "black");
+ $("#mon_label").css("color", "black");
+ $("#dow_label").css("color", "black");
+}
+
+var cron_minutes_id = "#minute_input";
+var cron_hours_id = "#hour_input";
+var cron_dom_id = "#dom_input";
+var cron_months_id = "#month_input";
+var cron_dow_id = "#dow_input";
+var cron_output_id = "#cron-output";
+var cron_translate_id = "#cronTranslate";
+var cron_translate_warning_id = "#translationWarning";
+
+// Cron use 0-6 as Sun--Sat, but Quartz use 1-7. Therefore, a translation is necessary.
+function transformFromCronToQuartz(str){
+ var res = str.split(" ");
+ res[res.length -1] = res[res.length -1].replace(/[0-7]/g, function upperToHyphenLower(match) {
+ return (parseInt(match)+6)%7;
+ });
+ return res.join(" ");
+}
+
+function updateOutput() {
+ $(cron_output_id).val( $(cron_minutes_id).val() + " " + $(cron_hours_id).val() + " " +
+ $(cron_dom_id).val() + " " + $(cron_months_id).val() + " " + $(cron_dow_id).val()
+ );
+ updateExpression();
+}
+
+function updateExpression() {
+ $('#nextRecurId').html("");
+
+ console.log("cron Input = " + $(cron_output_id).val());
+ var laterCron = later.parse.cron($(cron_output_id).val());
+
+ //Get the current time given the server timezone.
+ var serverTime = moment().tz(timezone);
+ console.log("serverTime = " + serverTime.format());
+ var now1Str = serverTime.format();
+
+ //Get the server Timezone offset against UTC (e.g. if timezone is PDT, it should be -07:00)
+ // var timeZoneOffset = now1Str.substring(now1Str.length-6, now1Str.length);
+ // console.log("offset = " + timeZoneOffset);
+
+ //Transform the moment time to UTC Date time (required by later.js)
+ var serverTimeInJsDateFormat = new Date();
+ serverTimeInJsDateFormat.setUTCHours(serverTime.get('hour'), serverTime.get('minute'), 0, 0);
+ serverTimeInJsDateFormat.setUTCMonth(serverTime.get('month'), serverTime.get('date'));
+
+ // Calculate the following 10 occurences based on the current server time.
+ // The logic is a bit tricky here. since later.js only support UTC Date (javascript raw library).
+ // We transform from current browser-timezone-time to Server timezone.
+ // Then we let serverTimeInJsDateFormat is equal to the server time.
+ var occurrences = later.schedule(laterCron).next(10, serverTimeInJsDateFormat);
+
+ //The following component below displays a list of next 10 triggering timestamp.
+ for(var i = 9; i >= 0; i--) {
+ var strTime = JSON.stringify(occurrences[i]);
+
+ // Get the time. The original occurance time string is like: "2016-09-09T05:00:00.999",
+ // We trim the string to ignore milliseconds.
+ var nextTime = '<li style="color:DarkGreen">' + strTime.substring(1, strTime.length-6) + '</li>';
+ $('#nextRecurId').prepend(nextTime);
+ }
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.js
new file mode 100644
index 0000000..249f1e4
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2012 LinkedIn Corp.
+ *
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var schedulePanelView;
+azkaban.SchedulePanelView = Backbone.View.extend({
+ events: {
+ "click #schedule-button": "scheduleFlow"
+ },
+
+ initialize: function(settings) {
+ $("#timepicker").datetimepicker({format: 'LT'});
+ $("#datepicker").datetimepicker({format: 'L'});
+ },
+
+ render: function() {
+ },
+
+ showSchedulePanel: function() {
+ $('#schedule-modal').modal();
+ },
+
+ hideSchedulePanel: function() {
+ $('#schedule-modal').modal("hide");
+ },
+
+ scheduleFlow: function() {
+ var timeVal = $('#timepicker').val();
+ var timezoneVal = $('#timezone').val();
+
+ var dateVal = $('#datepicker').val();
+
+ var is_recurringVal = $('#is_recurring').val();
+ var periodVal = $('#period').val();
+ var periodUnits = $('#period_units').val();
+
+ var scheduleURL = contextURL + "/schedule"
+ var scheduleData = flowExecuteDialogView.getExecutionOptionData();
+
+ console.log("Creating schedule for " + projectName + "." +
+ scheduleData.flow);
+
+ var scheduleTime = moment(timeVal, 'h/mm A').format('h,mm,A,') + timezoneVal;
+ console.log(scheduleTime);
+
+ var scheduleDate = $('#datepicker').val();
+ var is_recurring = document.getElementById('is_recurring').checked
+ ? 'on' : 'off';
+ var period = $('#period').val() + $('#period_units').val();
+
+ scheduleData.ajax = "scheduleFlow";
+ scheduleData.projectName = projectName;
+ scheduleData.scheduleTime = scheduleTime;
+ scheduleData.scheduleDate = scheduleDate;
+ scheduleData.is_recurring = is_recurring;
+ scheduleData.period = period;
+
+ var successHandler = function(data) {
+ if (data.error) {
+ schedulePanelView.hideSchedulePanel();
+ messageDialogView.show("Error Scheduling Flow", data.message);
+ }
+ else {
+ schedulePanelView.hideSchedulePanel();
+ messageDialogView.show("Flow Scheduled", data.message, function() {
+ window.location.href = scheduleURL;
+ });
+ }
+ };
+
+ $.post(scheduleURL, scheduleData, successHandler, "json");
+ }
+});
+
+$(function() {
+ schedulePanelView = new azkaban.SchedulePanelView({
+ el: $('#schedule-modal')
+ });
+});