+ * "At 8:00am every Monday through Friday" or "At 1:30am every
+ * last Friday of the month".
+ *
* Cron expressions are comprised of 6 required fields and one optional field
* separated by white space. The fields respectively are described as follows:
- *
- *
+ *
+ *
+ * Examples of cron expressions and their meanings.
*
- * | Field Name |
- * |
- * Allowed Values |
- * |
- * Allowed Special Characters |
+ * Field Name |
+ * |
+ * Allowed Values |
+ * |
+ * Allowed Special Characters |
*
*
- * Seconds |
- *
- * | 0-59 |
- *
- * | , - * / |
+ * Seconds |
+ * |
+ * 0-59 |
+ * |
+ * , - * / |
*
*
- * Minutes |
- *
- * | 0-59 |
- *
- * | , - * / |
+ * Minutes |
+ * |
+ * 0-59 |
+ * |
+ * , - * / |
*
*
- * Hours |
- *
- * | 0-23 |
- *
- * | , - * / |
+ * Hours |
+ * |
+ * 0-23 |
+ * |
+ * , - * / |
*
*
- * Day-of-month |
- *
- * | 1-31 |
- *
- * | , - * ? / L W |
+ * Day-of-month |
+ * |
+ * 1-31 |
+ * |
+ * , - * ? / L W |
*
*
- * Month |
- *
- * | 0-11 or JAN-DEC |
- *
- * | , - * / |
+ * Month |
+ * |
+ * 1-12 or JAN-DEC |
+ * |
+ * , - * / |
*
*
- * Day-of-Week |
- *
- * | 1-7 or SUN-SAT |
- *
- * | , - * ? / L # |
+ * Day-of-Week |
+ * |
+ * 1-7 or SUN-SAT |
+ * |
+ * , - * ? / L # |
*
*
- * Year (Optional) |
- *
- * | empty, 1970-2199 |
- *
- * | , - * / |
+ * Year (Optional) |
+ * |
+ * empty, 1970-2199 |
+ * |
+ * , - * / |
*
*
- *
- * The '*' character is used to specify all values. For example, "*"
+ *
+ * The '*' character is used to specify all values. For example, "*"
* in the minute field means "every minute".
- *
+ *
+ *
* The '?' character is allowed for the day-of-month and day-of-week fields. It
* is used to specify 'no specific value'. This is useful when you need to
* specify something in one of the two fields, but not the other.
- *
+ *
* The '-' character is used to specify ranges For example "10-12" in
* the hour field means "the hours 10, 11 and 12".
- *
+ *
* The ',' character is used to specify additional values. For example
* "MON,WED,FRI" in the day-of-week field means "the days Monday,
* Wednesday, and Friday".
- *
+ *
+ *
* The '/' character is used to specify increments. For example "0/15"
- * in the seconds field means "the seconds 0, 15, 30, and 45". And
+ * in the seconds field means "the seconds 0, 15, 30, and 45". And
* "5/15" in the seconds field means "the seconds 5, 20, 35, and
* 50". Specifying '*' before the '/' is equivalent to specifying 0 is
* the value to start with. Essentially, for each field in the expression, there
- * is a set of numbers that can be turned on or off. For seconds and minutes,
+ * is a set of numbers that can be turned on or off. For seconds and minutes,
* the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
* 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn
* on every "nth" value in the given set. Thus "7/6" in the
- * month field only turns on month "7", it does NOT mean every 6th
- * month, please note that subtlety.
- *
+ * month field only turns on month "7", it does NOT mean every 6th
+ * month, please note that subtlety.
+ *
+ *
* The 'L' character is allowed for the day-of-month and day-of-week fields.
- * This character is short-hand for "last", but it has different
- * meaning in each of the two fields. For example, the value "L" in
- * the day-of-month field means "the last day of the month" - day 31
- * for January, day 28 for February on non-leap years. If used in the
- * day-of-week field by itself, it simply means "7" or
+ * This character is short-hand for "last", but it has different
+ * meaning in each of the two fields. For example, the value "L" in
+ * the day-of-month field means "the last day of the month" - day 31
+ * for January, day 28 for February on non-leap years. If used in the
+ * day-of-week field by itself, it simply means "7" or
* "SAT". But if used in the day-of-week field after another value, it
* means "the last xxx day of the month" - for example "6L"
- * means "the last friday of the month". You can also specify an offset
- * from the last day of the month, such as "L-3" which would mean the third-to-last
- * day of the calendar month. When using the 'L' option, it is important not to
+ * means "the last friday of the month". You can also specify an offset
+ * from the last day of the month, such as "L-3" which would mean the third-to-last
+ * day of the calendar month. When using the 'L' option, it is important not to
* specify lists, or ranges of values, as you'll get confusing/unexpected results.
- *
- * The 'W' character is allowed for the day-of-month field. This character
- * is used to specify the weekday (Monday-Friday) nearest the given day. As an
- * example, if you were to specify "15W" as the value for the
+ *
+ *
+ * The 'W' character is allowed for the day-of-month field. This character
+ * is used to specify the weekday (Monday-Friday) nearest the given day. As an
+ * example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of
- * the month". So if the 15th is a Saturday, the trigger will fire on
+ * the month". So if the 15th is a Saturday, the trigger will fire on
* Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
- * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
+ * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
* However if you specify "1W" as the value for day-of-month, and the
- * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
- * 'jump' over the boundary of a month's days. The 'W' character can only be
+ * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
+ * 'jump' over the boundary of a month's days. The 'W' character can only be
* specified when the day-of-month is a single day, not a range or list of days.
- *
- * The 'L' and 'W' characters can also be combined for the day-of-month
- * expression to yield 'LW', which translates to "last weekday of the
+ *
+ *
+ * The 'L' and 'W' characters can also be combined for the day-of-month
+ * expression to yield 'LW', which translates to "last weekday of the
* month".
- *
+ *
+ *
* The '#' character is allowed for the day-of-week field. This character is
- * used to specify "the nth" XXX day of the month. For example, the
- * value of "6#3" in the day-of-week field means the third Friday of
- * the month (day 6 = Friday and "#3" = the 3rd one in the month).
- * Other examples: "2#1" = the first Monday of the month and
+ * used to specify "the nth" XXX day of the month. For example, the
+ * value of "6#3" in the day-of-week field means the third Friday of
+ * the month (day 6 = Friday and "#3" = the 3rd one in the month).
+ * Other examples: "2#1" = the first Monday of the month and
* "4#5" = the fifth Wednesday of the month. Note that if you specify
* "#5" and there is not 5 of the given day-of-week in the month, then
* no firing will occur that month. If the '#' character is used, there can
- * only be one expression in the day-of-week field ("3#1,6#3" is
+ * only be one expression in the day-of-week field ("3#1,6#3" is
* not valid, since there are two expressions).
- *
+ *
*
- *
+ *
* The legal characters and the names of months and days of the week are not
* case sensitive.
- *
+ *
*
* NOTES:
+ *
*
* - Support for specifying both a day-of-week and a day-of-month value is
* not complete (you'll need to use the '?' character in one of these fields).
*
- * - Overflowing ranges is supported - that is, having a larger number on
- * the left hand side than the right. You might do 22-2 to catch 10 o'clock
- * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
- * very important to note that overuse of overflowing ranges creates ranges
- * that don't make sense and no effort has been made to determine which
- * interpretation CronExpression chooses. An example would be
+ *
- Overflowing ranges is supported - that is, having a larger number on
+ * the left hand side than the right. You might do 22-2 to catch 10 o'clock
+ * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
+ * very important to note that overuse of overflowing ranges creates ranges
+ * that don't make sense and no effort has been made to determine which
+ * interpretation CronExpression chooses. An example would be
* "0 0 14-6 ? * FRI-MON".
*
- *
- *
- *
+ *
+ *
* @author Sharada Jambula, James House
* @author Contributions from Mads Henderson
* @author Refactoring from CronTrigger to CronExpression by Aaron Craven
- *
- * Borrowed from quartz v2.3.1
- *
*/
public final class CronExpression implements Serializable, Cloneable {
private static final long serialVersionUID = 12423409423L;
-
+
protected static final int SECOND = 0;
protected static final int MINUTE = 1;
protected static final int HOUR = 2;
@@ -212,11 +218,14 @@ public final class CronExpression implements Serializable, Cloneable {
protected static final int YEAR = 6;
protected static final int ALL_SPEC_INT = 99; // '*'
protected static final int NO_SPEC_INT = 98; // '?'
+ protected static final int MAX_LAST_DAY_OFFSET = 30;
+ protected static final int LAST_DAY_OFFSET_START = 32; // "L-30"
+ protected static final int LAST_DAY_OFFSET_END = LAST_DAY_OFFSET_START + MAX_LAST_DAY_OFFSET; // 'L'
protected static final Integer ALL_SPEC = ALL_SPEC_INT;
protected static final Integer NO_SPEC = NO_SPEC_INT;
-
- protected static final Map monthMap = new HashMap(20);
- protected static final Map dayMap = new HashMap(60);
+
+ protected static final Map monthMap = new HashMap<>(20);
+ protected static final Map dayMap = new HashMap<>(60);
static {
monthMap.put("JAN", 0);
monthMap.put("FEB", 1);
@@ -246,43 +255,41 @@ public final class CronExpression implements Serializable, Cloneable {
protected transient TreeSet minutes;
protected transient TreeSet hours;
protected transient TreeSet daysOfMonth;
+ protected transient TreeSet nearestWeekdays;
protected transient TreeSet months;
protected transient TreeSet daysOfWeek;
protected transient TreeSet years;
- protected transient boolean lastdayOfWeek = false;
- protected transient int nthdayOfWeek = 0;
- protected transient boolean lastdayOfMonth = false;
- protected transient boolean nearestWeekday = false;
- protected transient int lastdayOffset = 0;
+ protected transient boolean lastDayOfWeek = false;
+ protected transient int nthDayOfWeek = 0;
protected transient boolean expressionParsed = false;
-
+
public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
/**
- * Constructs a new CronExpression based on the specified
+ * Constructs a new CronExpression based on the specified
* parameter.
- *
+ *
* @param cronExpression String representation of the cron expression the
* new object should represent
* @throws java.text.ParseException
- * if the string expression cannot be parsed into a valid
+ * if the string expression cannot be parsed into a valid
* CronExpression
*/
public CronExpression(String cronExpression) throws ParseException {
if (cronExpression == null) {
throw new IllegalArgumentException("cronExpression cannot be null");
}
-
+
this.cronExpression = cronExpression.toUpperCase(Locale.US);
-
+
buildExpression(this.cronExpression);
}
-
+
/**
* Constructs a new {@code CronExpression} as a copy of an existing
* instance.
- *
+ *
* @param expression
* The existing cron expression to be copied
*/
@@ -296,7 +303,7 @@ public final class CronExpression implements Serializable, Cloneable {
try {
buildExpression(cronExpression);
} catch (ParseException ex) {
- throw new AssertionError();
+ throw new AssertionError("Could not parse expression!", ex);
}
if (expression.getTimeZone() != null) {
setTimeZone((TimeZone) expression.getTimeZone().clone());
@@ -307,7 +314,7 @@ public final class CronExpression implements Serializable, Cloneable {
* Indicates whether the given date satisfies the cron expression. Note that
* milliseconds are ignored, so two Dates falling on different milliseconds
* of the same second will always have the same result here.
- *
+ *
* @param date the date to evaluate
* @return a boolean indicating whether the given date satisfies the cron
* expression
@@ -317,18 +324,18 @@ public final class CronExpression implements Serializable, Cloneable {
testDateCal.setTime(date);
testDateCal.set(Calendar.MILLISECOND, 0);
Date originalDate = testDateCal.getTime();
-
+
testDateCal.add(Calendar.SECOND, -1);
-
+
Date timeAfter = getTimeAfter(testDateCal.getTime());
return ((timeAfter != null) && (timeAfter.equals(originalDate)));
}
-
+
/**
* Returns the next date/time after the given date/time which
* satisfies the cron expression.
- *
+ *
* @param date the date/time at which to begin the search for the next valid
* date/time
* @return the next valid date/time
@@ -336,28 +343,28 @@ public final class CronExpression implements Serializable, Cloneable {
public Date getNextValidTimeAfter(Date date) {
return getTimeAfter(date);
}
-
+
/**
* Returns the next date/time after the given date/time which does
* not satisfy the expression
- *
- * @param date the date/time at which to begin the search for the next
+ *
+ * @param date the date/time at which to begin the search for the next
* invalid date/time
* @return the next valid date/time
*/
public Date getNextInvalidTimeAfter(Date date) {
long difference = 1000;
-
+
//move back to the nearest second so differences will be accurate
Calendar adjustCal = Calendar.getInstance(getTimeZone());
adjustCal.setTime(date);
adjustCal.set(Calendar.MILLISECOND, 0);
Date lastDate = adjustCal.getTime();
-
+
Date newDate;
-
+
//FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
-
+
//keep getting the next included time until it's farther than one second
// apart. At that point, lastDate is the last valid fire time. We return
// the second immediately following it.
@@ -365,19 +372,19 @@ public final class CronExpression implements Serializable, Cloneable {
newDate = getTimeAfter(lastDate);
if(newDate == null)
break;
-
+
difference = newDate.getTime() - lastDate.getTime();
-
+
if (difference == 1000) {
lastDate = newDate;
}
}
-
+
return new Date(lastDate.getTime() + 1000);
}
-
+
/**
- * Returns the time zone for which this CronExpression
+ * Returns the time zone for which this CronExpression
* will be resolved.
*/
public TimeZone getTimeZone() {
@@ -389,16 +396,16 @@ public final class CronExpression implements Serializable, Cloneable {
}
/**
- * Sets the time zone for which this CronExpression
+ * Sets the time zone for which this CronExpression
* will be resolved.
*/
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
-
+
/**
* Returns the string representation of the CronExpression
- *
+ *
* @return a string representation of the CronExpression
*/
@Override
@@ -407,30 +414,30 @@ public final class CronExpression implements Serializable, Cloneable {
}
/**
- * Indicates whether the specified cron expression can be parsed into a
+ * Indicates whether the specified cron expression can be parsed into a
* valid cron expression
- *
+ *
* @param cronExpression the expression to evaluate
* @return a boolean indicating whether the given expression is a valid cron
* expression
*/
public static boolean isValidExpression(String cronExpression) {
-
+
try {
new CronExpression(cronExpression);
} catch (ParseException pe) {
return false;
}
-
+
return true;
}
public static void validateExpression(String cronExpression) throws ParseException {
-
+
new CronExpression(cronExpression);
}
-
-
+
+
////////////////////////////////////////////////////////////////////////////
//
// Expression Parsing Functions
@@ -443,25 +450,28 @@ public final class CronExpression implements Serializable, Cloneable {
try {
if (seconds == null) {
- seconds = new TreeSet();
+ seconds = new TreeSet<>();
}
if (minutes == null) {
- minutes = new TreeSet();
+ minutes = new TreeSet<>();
}
if (hours == null) {
- hours = new TreeSet();
+ hours = new TreeSet<>();
}
if (daysOfMonth == null) {
- daysOfMonth = new TreeSet();
+ daysOfMonth = new TreeSet<>();
+ }
+ if (nearestWeekdays == null) {
+ nearestWeekdays = new TreeSet<>();
}
if (months == null) {
- months = new TreeSet();
+ months = new TreeSet<>();
}
if (daysOfWeek == null) {
- daysOfWeek = new TreeSet();
+ daysOfWeek = new TreeSet<>();
}
if (years == null) {
- years = new TreeSet();
+ years = new TreeSet<>();
}
int exprOn = SECOND;
@@ -469,13 +479,13 @@ public final class CronExpression implements Serializable, Cloneable {
StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
false);
+ if(exprsTok.countTokens() > 7) {
+ throw new ParseException("Invalid expression has too many terms: " + expression, -1);
+ }
+
while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
String expr = exprsTok.nextToken().trim();
- // throw an exception if L is used with other days of the month
- if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
- throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
- }
// throw an exception if L is used with other days of the week
if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
@@ -483,7 +493,7 @@ public final class CronExpression implements Serializable, Cloneable {
if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) {
throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
}
-
+
StringTokenizer vTok = new StringTokenizer(expr, ",");
while (vTok.hasMoreTokens()) {
String v = vTok.nextToken();
@@ -495,7 +505,7 @@ public final class CronExpression implements Serializable, Cloneable {
if (exprOn <= DAY_OF_WEEK) {
throw new ParseException("Unexpected end of expression.",
- expression.length());
+ expression.length());
}
if (exprOn <= YEAR) {
@@ -519,12 +529,12 @@ public final class CronExpression implements Serializable, Cloneable {
throw pe;
} catch (Exception e) {
throw new ParseException("Illegal cron expression format ("
- + e.toString() + ")", 0);
+ + e + ")", 0);
}
}
protected int storeExpressionVals(int pos, String s, int type)
- throws ParseException {
+ throws ParseException {
int incr = 0;
int i = skipWhiteSpace(pos, s);
@@ -556,7 +566,7 @@ public final class CronExpression implements Serializable, Cloneable {
sval = getDayOfWeekNumber(sub);
if (sval < 0) {
throw new ParseException("Invalid Day-of-Week value: '"
- + sub + "'", i);
+ + sub + "'", i);
}
if (s.length() > i + 3) {
c = s.charAt(i + 3);
@@ -567,13 +577,13 @@ public final class CronExpression implements Serializable, Cloneable {
if (eval < 0) {
throw new ParseException(
"Invalid Day-of-Week value: '" + sub
- + "'", i);
+ + "'", i);
}
} else if (c == '#') {
try {
i += 4;
- nthdayOfWeek = Integer.parseInt(s.substring(i));
- if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+ nthDayOfWeek = Integer.parseInt(s.substring(i));
+ if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
throw new Exception();
}
} catch (Exception e) {
@@ -582,7 +592,7 @@ public final class CronExpression implements Serializable, Cloneable {
i);
}
} else if (c == 'L') {
- lastdayOfWeek = true;
+ lastDayOfWeek = true;
i++;
}
}
@@ -601,22 +611,21 @@ public final class CronExpression implements Serializable, Cloneable {
if (c == '?') {
i++;
- if ((i + 1) < s.length()
+ if ((i + 1) < s.length()
&& (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
throw new ParseException("Illegal character after '?': "
- + s.charAt(i), i);
+ + s.charAt(i), i);
}
if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
throw new ParseException(
- "'?' can only be specified for Day-of-Month or Day-of-Week.",
- i);
+ "'?' can only be specified for Day-of-Month or Day-of-Week.",
+ i);
}
- if (type == DAY_OF_WEEK && !lastdayOfMonth) {
- int val = daysOfMonth.last();
- if (val == NO_SPEC_INT) {
+ if (type == DAY_OF_WEEK) {
+ if (!daysOfMonth.isEmpty() && daysOfMonth.last() == NO_SPEC_INT) {
throw new ParseException(
- "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
- i);
+ "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
+ i);
}
}
@@ -630,7 +639,7 @@ public final class CronExpression implements Serializable, Cloneable {
return i + 1;
} else if (c == '/'
&& ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
- .charAt(i + 1) == '\t')) {
+ .charAt(i + 1) == '\t')) {
throw new ParseException("'/' must be followed by an integer.", i);
} else if (c == '*') {
i++;
@@ -656,29 +665,40 @@ public final class CronExpression implements Serializable, Cloneable {
addToSet(ALL_SPEC_INT, -1, incr, type);
return i;
} else if (c == 'L') {
+
+ if(type < DAY_OF_MONTH)
+ throw new ParseException("'L' not expected in seconds, minutes or hours fields.", i);
+
i++;
- if (type == DAY_OF_MONTH) {
- lastdayOfMonth = true;
- }
if (type == DAY_OF_WEEK) {
addToSet(7, 7, 0, type);
}
- if(type == DAY_OF_MONTH && s.length() > i) {
- c = s.charAt(i);
- if(c == '-') {
- ValueSet vs = getValue(0, s, i+1);
- lastdayOffset = vs.value;
- if(lastdayOffset > 30)
- throw new ParseException("Offset from last day must be <= 30", i+1);
- i = vs.pos;
- }
- if(s.length() > i) {
+ if (type == DAY_OF_MONTH) {
+ int dom = LAST_DAY_OFFSET_END;
+ boolean nearestWeekday = false;
+ if (s.length() > i) {
c = s.charAt(i);
- if(c == 'W') {
- nearestWeekday = true;
- i++;
+ if (c == '-') {
+ ValueSet vs = getValue(0, s, i + 1);
+ int offset = vs.value;
+ if (offset > MAX_LAST_DAY_OFFSET)
+ throw new ParseException("Offset from last day must be <= " + MAX_LAST_DAY_OFFSET, i + 1);
+ dom -= offset;
+ i = vs.pos;
+ }
+ if (s.length() > i) {
+ c = s.charAt(i);
+ if (c == 'W') {
+ nearestWeekday = true;
+ i++;
+ }
}
}
+ if (nearestWeekday) {
+ nearestWeekdays.add(dom);
+ } else {
+ daysOfMonth.add(dom);
+ }
}
return i;
} else if (c >= '0' && c <= '9') {
@@ -705,21 +725,21 @@ public final class CronExpression implements Serializable, Cloneable {
private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
if (incr > 59 && (type == SECOND || type == MINUTE)) {
- throw new ParseException("Increment > 60 : " + incr, idxPos);
+ throw new ParseException("Increment >= 60 : " + incr, idxPos);
} else if (incr > 23 && (type == HOUR)) {
- throw new ParseException("Increment > 24 : " + incr, idxPos);
+ throw new ParseException("Increment >= 24 : " + incr, idxPos);
} else if (incr > 31 && (type == DAY_OF_MONTH)) {
- throw new ParseException("Increment > 31 : " + incr, idxPos);
+ throw new ParseException("Increment >= 31 : " + incr, idxPos);
} else if (incr > 7 && (type == DAY_OF_WEEK)) {
- throw new ParseException("Increment > 7 : " + incr, idxPos);
+ throw new ParseException("Increment >= 7 : " + incr, idxPos);
} else if (incr > 12 && (type == MONTH)) {
- throw new ParseException("Increment > 12 : " + incr, idxPos);
+ throw new ParseException("Increment >= 12 : " + incr, idxPos);
}
}
protected int checkNext(int pos, String s, int val, int type)
- throws ParseException {
-
+ throws ParseException {
+
int end = -1;
int i = pos;
@@ -734,7 +754,7 @@ public final class CronExpression implements Serializable, Cloneable {
if (type == DAY_OF_WEEK) {
if(val < 1 || val > 7)
throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
- lastdayOfWeek = true;
+ lastDayOfWeek = true;
} else {
throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
}
@@ -743,17 +763,14 @@ public final class CronExpression implements Serializable, Cloneable {
i++;
return i;
}
-
+
if (c == 'W') {
- if (type == DAY_OF_MONTH) {
- nearestWeekday = true;
- } else {
+ if (type != DAY_OF_MONTH) {
throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
}
if(val > 31)
- throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
- TreeSet set = getSet(type);
- set.add(val);
+ throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
+ nearestWeekdays.add(val);
i++;
return i;
}
@@ -764,8 +781,8 @@ public final class CronExpression implements Serializable, Cloneable {
}
i++;
try {
- nthdayOfWeek = Integer.parseInt(s.substring(i));
- if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+ nthDayOfWeek = Integer.parseInt(s.substring(i));
+ if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
throw new Exception();
}
} catch (Exception e) {
@@ -857,7 +874,7 @@ public final class CronExpression implements Serializable, Cloneable {
public String getCronExpression() {
return cronExpression;
}
-
+
public String getExpressionSummary() {
StringBuilder buf = new StringBuilder();
@@ -873,23 +890,20 @@ public final class CronExpression implements Serializable, Cloneable {
buf.append("daysOfMonth: ");
buf.append(getExpressionSetSummary(daysOfMonth));
buf.append("\n");
+ buf.append("nearestWeekdays: ");
+ buf.append(getExpressionSetSummary(nearestWeekdays));
+ buf.append("\n");
buf.append("months: ");
buf.append(getExpressionSetSummary(months));
buf.append("\n");
buf.append("daysOfWeek: ");
buf.append(getExpressionSetSummary(daysOfWeek));
buf.append("\n");
- buf.append("lastdayOfWeek: ");
- buf.append(lastdayOfWeek);
- buf.append("\n");
- buf.append("nearestWeekday: ");
- buf.append(nearestWeekday);
+ buf.append("lastDayOfWeek: ");
+ buf.append(lastDayOfWeek);
buf.append("\n");
buf.append("NthDayOfWeek: ");
- buf.append(nthdayOfWeek);
- buf.append("\n");
- buf.append("lastdayOfMonth: ");
- buf.append(lastdayOfMonth);
+ buf.append(nthDayOfWeek);
buf.append("\n");
buf.append("years: ");
buf.append(getExpressionSetSummary(years));
@@ -965,8 +979,8 @@ public final class CronExpression implements Serializable, Cloneable {
}
protected void addToSet(int val, int end, int incr, int type)
- throws ParseException {
-
+ throws ParseException {
+
TreeSet set = getSet(type);
if (type == SECOND || type == MINUTE) {
@@ -981,7 +995,7 @@ public final class CronExpression implements Serializable, Cloneable {
"Hour values must be between 0 and 23", -1);
}
} else if (type == DAY_OF_MONTH) {
- if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
+ if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
&& (val != NO_SPEC_INT)) {
throw new ParseException(
"Day of month values must be between 1 and 31", -1);
@@ -1005,7 +1019,7 @@ public final class CronExpression implements Serializable, Cloneable {
} else {
set.add(NO_SPEC);
}
-
+
return;
}
@@ -1061,20 +1075,20 @@ public final class CronExpression implements Serializable, Cloneable {
}
}
- // if the end of the range is before the start, then we need to overflow into
- // the next day, month etc. This is done by adding the maximum amount for that
+ // if the end of the range is before the start, then we need to overflow into
+ // the next day, month etc. This is done by adding the maximum amount for that
// type, and using modulus max to determine the value being added.
int max = -1;
if (stopAt < startAt) {
switch (type) {
- case SECOND : max = 60; break;
- case MINUTE : max = 60; break;
- case HOUR : max = 24; break;
- case MONTH : max = 12; break;
- case DAY_OF_WEEK : max = 7; break;
- case DAY_OF_MONTH : max = 31; break;
- case YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
- default : throw new IllegalArgumentException("Unexpected type encountered");
+ case SECOND : max = 60; break;
+ case MINUTE : max = 60; break;
+ case HOUR : max = 24; break;
+ case MONTH : max = 12; break;
+ case DAY_OF_WEEK : max = 7; break;
+ case DAY_OF_MONTH : max = 31; break;
+ case YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
+ default : throw new IllegalArgumentException("Unexpected type encountered");
}
stopAt += max;
}
@@ -1130,7 +1144,7 @@ public final class CronExpression implements Serializable, Cloneable {
c = s.charAt(i);
}
ValueSet val = new ValueSet();
-
+
val.pos = (i < s.length()) ? i : i + 1;
val.value = Integer.parseInt(s1.toString());
return val;
@@ -1171,7 +1185,7 @@ public final class CronExpression implements Serializable, Cloneable {
public Date getTimeAfter(Date afterTime) {
// Computation is based on Gregorian year only.
- Calendar cl = new java.util.GregorianCalendar(getTimeZone());
+ Calendar cl = new java.util.GregorianCalendar(getTimeZone());
// move ahead one second, since we're computing the time *after* the
// given time
@@ -1197,7 +1211,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get second.................................................
st = seconds.tailSet(sec);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
sec = st.first();
} else {
sec = seconds.first();
@@ -1212,7 +1226,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get minute.................................................
st = minutes.tailSet(min);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = min;
min = st.first();
} else {
@@ -1233,7 +1247,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get hour...................................................
st = hours.tailSet(hr);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = hr;
hr = st.first();
} else {
@@ -1255,66 +1269,17 @@ public final class CronExpression implements Serializable, Cloneable {
// 1-based
t = -1;
int tmon = mon;
-
+
// get day...................................................
boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
- st = daysOfMonth.tailSet(day);
- if (lastdayOfMonth) {
- if(!nearestWeekday) {
- t = day;
- day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- day -= lastdayOffset;
- if(t > day) {
- mon++;
- if(mon > 12) {
- mon = 1;
- tmon = 3333; // ensure test of mon != tmon further below fails
- cl.add(Calendar.YEAR, 1);
- }
- day = 1;
- }
- } else {
- t = day;
- day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- day -= lastdayOffset;
-
- java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
- tcal.set(Calendar.SECOND, 0);
- tcal.set(Calendar.MINUTE, 0);
- tcal.set(Calendar.HOUR_OF_DAY, 0);
- tcal.set(Calendar.DAY_OF_MONTH, day);
- tcal.set(Calendar.MONTH, mon - 1);
- tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
-
- int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- int dow = tcal.get(Calendar.DAY_OF_WEEK);
-
- if(dow == Calendar.SATURDAY && day == 1) {
- day += 2;
- } else if(dow == Calendar.SATURDAY) {
- day -= 1;
- } else if(dow == Calendar.SUNDAY && day == ldom) {
- day -= 2;
- } else if(dow == Calendar.SUNDAY) {
- day += 1;
- }
-
- tcal.set(Calendar.SECOND, sec);
- tcal.set(Calendar.MINUTE, min);
- tcal.set(Calendar.HOUR_OF_DAY, hr);
- tcal.set(Calendar.DAY_OF_MONTH, day);
- tcal.set(Calendar.MONTH, mon - 1);
- Date nTime = tcal.getTime();
- if(nTime.before(afterTime)) {
- day = 1;
- mon++;
- }
- }
- } else if(nearestWeekday) {
- t = day;
- day = daysOfMonth.first();
+ Optional smallestDay = findSmallestDay(day, mon, cl.get(Calendar.YEAR), daysOfMonth);
+ Optional smallestDayForWeekday = findSmallestDay(day, mon, cl.get(Calendar.YEAR), nearestWeekdays);
+ t = day;
+ day = -1;
+ if (smallestDayForWeekday.isPresent()) {
+ day = smallestDayForWeekday.get();
java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
tcal.set(Calendar.SECOND, 0);
@@ -1323,7 +1288,7 @@ public final class CronExpression implements Serializable, Cloneable {
tcal.set(Calendar.DAY_OF_MONTH, day);
tcal.set(Calendar.MONTH, mon - 1);
tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
-
+
int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
int dow = tcal.get(Calendar.DAY_OF_WEEK);
@@ -1331,13 +1296,13 @@ public final class CronExpression implements Serializable, Cloneable {
day += 2;
} else if(dow == Calendar.SATURDAY) {
day -= 1;
- } else if(dow == Calendar.SUNDAY && day == ldom) {
+ } else if(dow == Calendar.SUNDAY && day == ldom) {
day -= 2;
- } else if(dow == Calendar.SUNDAY) {
+ } else if(dow == Calendar.SUNDAY) {
day += 1;
}
-
-
+
+
tcal.set(Calendar.SECOND, sec);
tcal.set(Calendar.MINUTE, min);
tcal.set(Calendar.HOUR_OF_DAY, hr);
@@ -1345,24 +1310,23 @@ public final class CronExpression implements Serializable, Cloneable {
tcal.set(Calendar.MONTH, mon - 1);
Date nTime = tcal.getTime();
if(nTime.before(afterTime)) {
- day = daysOfMonth.first();
- mon++;
+ day = -1;
}
- } else if (st != null && st.size() != 0) {
- t = day;
- day = st.first();
- // make sure we don't over-run a short month, such as february
- int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- if (day > lastDay) {
- day = daysOfMonth.first();
- mon++;
+ }
+
+ boolean needAdvance = false;
+ if (smallestDay.isPresent()) {
+ if (day == -1 || smallestDay.get() < day) {
+ day = smallestDay.get();
+ needAdvance = true;
}
- } else {
- day = daysOfMonth.first();
+ } else if (day == -1) {
+ day = 1;
mon++;
+ needAdvance = true;
}
-
- if (day != t || mon != tmon) {
+
+ if (needAdvance && (day != t || mon != tmon)) {
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1372,8 +1336,9 @@ public final class CronExpression implements Serializable, Cloneable {
// are 1-based
continue;
}
+
} else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
- if (lastdayOfWeek) { // are we looking for the last XXX day of
+ if (lastDayOfWeek) { // are we looking for the last XXX day of
// the month?
int dow = daysOfWeek.first(); // desired
// d-o-w
@@ -1416,7 +1381,7 @@ public final class CronExpression implements Serializable, Cloneable {
continue;
}
- } else if (nthdayOfWeek != 0) {
+ } else if (nthDayOfWeek != 0) {
// are we looking for the Nth XXX day in the month?
int dow = daysOfWeek.first(); // desired
// d-o-w
@@ -1428,10 +1393,7 @@ public final class CronExpression implements Serializable, Cloneable {
daysToAdd = dow + (7 - cDow);
}
- boolean dayShifted = false;
- if (daysToAdd > 0) {
- dayShifted = true;
- }
+ boolean dayShifted = daysToAdd > 0;
day += daysToAdd;
int weekOfMonth = day / 7;
@@ -1439,11 +1401,11 @@ public final class CronExpression implements Serializable, Cloneable {
weekOfMonth++;
}
- daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
+ daysToAdd = (nthDayOfWeek - weekOfMonth) * 7;
day += daysToAdd;
if (daysToAdd < 0
|| day > getLastDayOfMonth(mon, cl
- .get(Calendar.YEAR))) {
+ .get(Calendar.YEAR))) {
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1465,7 +1427,7 @@ public final class CronExpression implements Serializable, Cloneable {
int dow = daysOfWeek.first(); // desired
// d-o-w
st = daysOfWeek.tailSet(cDow);
- if (st != null && st.size() > 0) {
+ if (st != null && !st.isEmpty()) {
dow = st.first();
}
@@ -1488,7 +1450,7 @@ public final class CronExpression implements Serializable, Cloneable {
cl.set(Calendar.MONTH, mon);
// no '- 1' here because we are promoting the month
continue;
- } else if (daysToAdd > 0) { // are we swithing days?
+ } else if (daysToAdd > 0) { // are we switching days?
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1519,7 +1481,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get month...................................................
st = months.tailSet(mon);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = mon;
mon = st.first();
} else {
@@ -1546,7 +1508,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get year...................................................
st = years.tailSet(year);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = year;
year = st.first();
} else {
@@ -1575,7 +1537,7 @@ public final class CronExpression implements Serializable, Cloneable {
/**
* Advance the calendar to the particular hour paying particular attention
* to daylight saving problems.
- *
+ *
* @param cal the calendar to operate on
* @param hour the hour to set
*/
@@ -1587,23 +1549,64 @@ public final class CronExpression implements Serializable, Cloneable {
}
/**
- * NOT YET IMPLEMENTED: Returns the time before the given time
+ * Returns the time before the given time
* that the CronExpression matches.
- */
- public Date getTimeBefore(Date endTime) {
- // FUTURE_TODO: implement QUARTZ-423
- return null;
+ *
+ * @param endTime a time for which the previous
+ * matching time is returned
+ * @return the previous matching time before the given end time,
+ * or null if there are no previous matching times
+ */
+ public Date getTimeBefore(Date endTime) {
+ // the current implementation is not a direct calculation, but rather
+ // uses getTimeAfter with a binary search to find the previous match time
+ long end = endTime.getTime();
+ long min = 0; // the epoch date is the minimum supported by this class
+ long max = end;
+ // check if it's satisfiable at all
+ Date date = new Date(min);
+ Date after = getTimeAfter(date);
+ if (after == null || after.getTime() >= end)
+ return null; // there are no after-times before end
+ // from this point forward min's time-after is always less than end,
+ // and max's time-after is always equal to or greater than end
+ // so we just need to shrink the interval until they meet.
+ // optimization - perform inverse binary search to find a tighter lower bound
+ long interval = 60 * 60 * 1000; // start with a reasonable interval
+ while (interval < max) {
+ date.setTime(max - interval);
+ after = getTimeAfter(date);
+ if (after != null && after.getTime() < max) {
+ min = date.getTime(); // found a closer min
+ break;
+ }
+ interval *= 2;
+ }
+ // perform a regular binary search to find the earliest moment
+ // whose time-after is equal to or greater than the end time -
+ // this moment is the previous match time itself
+ while (max - min > 1000) { // we can stop at 1 second resolution
+ long mid = (min + max) >>> 1;
+ date.setTime(mid);
+ after = getTimeAfter(date);
+ if (after != null && after.getTime() < end)
+ min = mid;
+ else
+ max = mid;
+ }
+ date.setTime(max - max % 1000); // round to second
+ return date;
}
/**
- * NOT YET IMPLEMENTED: Returns the final time that the
+ * NOT YET IMPLEMENTED: Returns the final time that the
* CronExpression will match.
*/
public Date getFinalFireTime() {
// FUTURE_TODO: implement QUARTZ-423
return null;
}
-
+
protected boolean isLeapYear(int year) {
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
@@ -1640,18 +1643,43 @@ public final class CronExpression implements Serializable, Cloneable {
+ monthNum);
}
}
-
+
+
+ private Optional findSmallestDay(int day, int mon, int year, TreeSet set) {
+ if (set.isEmpty()) {
+ return Optional.empty();
+ }
+
+ final int lastDay = getLastDayOfMonth(mon, year);
+ // For "L", "L-1", etc.
+ final int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day)))
+ .map(d -> d - LAST_DAY_OFFSET_START + 1)
+ .orElse(Integer.MAX_VALUE);
+
+ // For "1", "2", etc.
+ SortedSet st = set.subSet(day, LAST_DAY_OFFSET_START);
+ // make sure we don't over-run a short month, such as february
+ if (!st.isEmpty() && st.first() < smallestDay && st.first() <= lastDay) {
+ return Optional.of(st.first());
+ }
+
+ if (smallestDay == Integer.MAX_VALUE) {
+ return Optional.empty();
+ } else {
+ return Optional.of(smallestDay + lastDay - LAST_DAY_OFFSET_START + 1);
+ }
+ }
private void readObject(java.io.ObjectInputStream stream)
- throws java.io.IOException, ClassNotFoundException {
-
+ throws java.io.IOException, ClassNotFoundException {
+
stream.defaultReadObject();
try {
buildExpression(cronExpression);
} catch (Exception ignore) {
} // never happens
- }
-
+ }
+
@Override
@Deprecated
public Object clone() {
@@ -1663,4 +1691,4 @@ class ValueSet {
public int value;
public int pos;
-}
+}
\ No newline at end of file
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/exception/XxlJobException.java
similarity index 79%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/exception/XxlJobException.java
index faa6063c..99bc85db 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/exception/XxlJobException.java
@@ -1,4 +1,4 @@
-package com.xxl.job.admin.core.exception;
+package com.xxl.job.admin.business.scheduler.exception;
/**
* @author xuxueli 2019-05-04 23:19:29
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireHandler.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireHandler.java
new file mode 100644
index 00000000..651a8e55
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireHandler.java
@@ -0,0 +1,17 @@
+package com.xxl.job.admin.business.scheduler.misfire;
+
+/**
+ * Misfire Handler
+ *
+ * @author xuxueli 2020-10-29
+ */
+public abstract class MisfireHandler {
+
+ /**
+ * misfire handle
+ *
+ * @param jobId jobId
+ */
+ public abstract void handle(final int jobId);
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireStrategyEnum.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireStrategyEnum.java
new file mode 100644
index 00000000..b7dfb018
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/MisfireStrategyEnum.java
@@ -0,0 +1,54 @@
+package com.xxl.job.admin.business.scheduler.misfire;
+
+import com.xxl.job.admin.business.scheduler.misfire.strategy.MisfireDoNothing;
+import com.xxl.job.admin.business.scheduler.misfire.strategy.MisfireFireOnceNow;
+import com.xxl.job.admin.framework.util.I18nUtil;
+
+/**
+ * @author xuxueli 2020-10-29 21:11:23
+ */
+public enum MisfireStrategyEnum {
+
+ /**
+ * do nothing
+ */
+ DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing"), new MisfireDoNothing()),
+
+ /**
+ * fire once now
+ */
+ FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now"), new MisfireFireOnceNow());
+
+ private final String title;
+ private final MisfireHandler misfireHandler;
+
+ MisfireStrategyEnum(String title, MisfireHandler misfireHandler) {
+ this.title = title;
+ this.misfireHandler = misfireHandler;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public MisfireHandler getMisfireHandler() {
+ return misfireHandler;
+ }
+
+ /**
+ * match misfire strategy
+ *
+ * @param name name of misfire strategy
+ * @param defaultItem default misfire strategy
+ * @return misfire strategy
+ */
+ public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){
+ for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) {
+ if (item.name().equals(name)) {
+ return item;
+ }
+ }
+ return defaultItem;
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireDoNothing.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireDoNothing.java
new file mode 100644
index 00000000..bc779ee2
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireDoNothing.java
@@ -0,0 +1,15 @@
+package com.xxl.job.admin.business.scheduler.misfire.strategy;
+
+import com.xxl.job.admin.business.scheduler.misfire.MisfireHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MisfireDoNothing extends MisfireHandler {
+ private static final Logger logger = LoggerFactory.getLogger(MisfireDoNothing.class);
+
+ @Override
+ public void handle(int jobId) {
+ logger.warn(">>>>>>>>>>> xxl-job, schedule MisfireDoNothing: jobId = " + jobId );
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireFireOnceNow.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireFireOnceNow.java
new file mode 100644
index 00000000..71aea1b8
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/misfire/strategy/MisfireFireOnceNow.java
@@ -0,0 +1,19 @@
+package com.xxl.job.admin.business.scheduler.misfire.strategy;
+
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.misfire.MisfireHandler;
+import com.xxl.job.admin.business.scheduler.trigger.TriggerTypeEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MisfireFireOnceNow extends MisfireHandler {
+ protected static Logger logger = LoggerFactory.getLogger(MisfireFireOnceNow.class);
+
+ @Override
+ public void handle(int jobId) {
+ // FIRE_ONCE_NOW 》 trigger
+ XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.MISFIRE, -1, null, null, null);
+ logger.warn(">>>>>>>>>>> xxl-job, schedule MisfireFireOnceNow: jobId = " + jobId );
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/openapi/OpenApiController.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/openapi/OpenApiController.java
new file mode 100644
index 00000000..895f72ee
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/openapi/OpenApiController.java
@@ -0,0 +1,80 @@
+package com.xxl.job.admin.business.scheduler.openapi;
+
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.core.constant.Const;
+import com.xxl.job.core.openapi.AdminBiz;
+import com.xxl.job.core.openapi.model.CallbackRequest;
+import com.xxl.job.core.openapi.model.RegistryRequest;
+import com.xxl.sso.core.annotation.XxlSso;
+import com.xxl.tool.core.StringTool;
+import com.xxl.tool.json.GsonTool;
+import com.xxl.tool.response.Response;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/5/10.
+ */
+@Controller
+public class OpenApiController {
+
+ @Resource
+ private AdminBiz adminBiz;
+
+ /**
+ * api
+ */
+ @RequestMapping("/api/{uri}")
+ @ResponseBody
+ @XxlSso(login = false)
+ public Object api(HttpServletRequest request,
+ @PathVariable("uri") String uri,
+ @RequestHeader(value = Const.XXL_JOB_ACCESS_TOKEN, required = false) String accesstoken,
+ @RequestBody(required = false) String requestBody) {
+
+ // valid
+ if (!"POST".equalsIgnoreCase(request.getMethod())) {
+ return Response.ofFail("invalid request, HttpMethod not support.");
+ }
+ if (StringTool.isBlank(uri)) {
+ return Response.ofFail("invalid request, uri-mapping empty.");
+ }
+ if (StringTool.isBlank(requestBody)) {
+ return Response.ofFail("invalid request, requestBody empty.");
+ }
+
+ // valid token
+ if (StringTool.isNotBlank(XxlJobAdminBootstrap.getInstance().getAccessToken())
+ && !XxlJobAdminBootstrap.getInstance().getAccessToken().equals(accesstoken)) {
+ return Response.ofFail("The access token is wrong.");
+ }
+
+ // dispatch request
+ try {
+ switch (uri) {
+ case "callback": {
+ List callbackParamList = GsonTool.fromJson(requestBody, List.class, CallbackRequest.class);
+ return adminBiz.callback(callbackParamList);
+ }
+ case "registry": {
+ RegistryRequest registryParam = GsonTool.fromJson(requestBody, RegistryRequest.class);
+ return adminBiz.registry(registryParam);
+ }
+ case "registryRemove": {
+ RegistryRequest registryParam = GsonTool.fromJson(requestBody, RegistryRequest.class);
+ return adminBiz.registryRemove(registryParam);
+ }
+ default:
+ return Response.ofFail("invalid request, uri-mapping("+ uri +") not found.");
+ }
+ } catch (Exception e) {
+ return Response.ofFail("openapi invoke error: " + e.getMessage());
+ }
+
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouteStrategyEnum.java
similarity index 89%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouteStrategyEnum.java
index 7fff93a9..6bcf1f3a 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouteStrategyEnum.java
@@ -1,7 +1,7 @@
-package com.xxl.job.admin.core.route;
+package com.xxl.job.admin.business.scheduler.route;
-import com.xxl.job.admin.core.route.strategy.*;
-import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.admin.business.scheduler.route.strategy.*;
+import com.xxl.job.admin.framework.util.I18nUtil;
/**
* Created by xuxueli on 17/3/10.
@@ -34,6 +34,9 @@ public enum ExecutorRouteStrategyEnum {
return router;
}
+ /**
+ * match router
+ */
public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){
if (name != null) {
for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouter.java
similarity index 53%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouter.java
index 5de9a1d0..2a8a4b2d 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/ExecutorRouter.java
@@ -1,7 +1,7 @@
-package com.xxl.job.admin.core.route;
+package com.xxl.job.admin.business.scheduler.route;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -16,9 +16,9 @@ public abstract class ExecutorRouter {
/**
* route address
*
- * @param addressList
+ * @param addressList executor address list
* @return ReturnT.content=address
*/
- public abstract ReturnT route(TriggerParam triggerParam, List addressList);
+ public abstract Response route(TriggerRequest triggerParam, List addressList);
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteBusyover.java
similarity index 51%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteBusyover.java
index 868560fc..2a04221f 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteBusyover.java
@@ -1,12 +1,12 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.admin.core.util.I18nUtil;
-import com.xxl.job.core.biz.ExecutorBiz;
-import com.xxl.job.core.biz.model.IdleBeatParam;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.admin.framework.util.I18nUtil;
+import com.xxl.job.core.openapi.ExecutorBiz;
+import com.xxl.job.core.openapi.model.IdleBeatRequest;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import java.util.List;
@@ -16,17 +16,17 @@ import java.util.List;
public class ExecutorRouteBusyover extends ExecutorRouter {
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
StringBuffer idleBeatResultSB = new StringBuffer();
for (String address : addressList) {
// beat
- ReturnT idleBeatResult = null;
+ Response idleBeatResult = null;
try {
- ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
- idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
+ ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address);
+ idleBeatResult = executorBiz.idleBeat(new IdleBeatRequest(triggerParam.getJobId()));
} catch (Exception e) {
logger.error(e.getMessage(), e);
- idleBeatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e );
+ idleBeatResult = Response.ofFail( ""+e );
}
idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"
":"")
.append(I18nUtil.getString("jobconf_idleBeat") + ":")
@@ -35,14 +35,14 @@ public class ExecutorRouteBusyover extends ExecutorRouter {
.append("
msg:").append(idleBeatResult.getMsg());
// beat success
- if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
+ if (idleBeatResult.isSuccess()) {
idleBeatResult.setMsg(idleBeatResultSB.toString());
- idleBeatResult.setContent(address);
+ idleBeatResult.setData(address);
return idleBeatResult;
}
}
- return new ReturnT(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
+ return Response.ofFail( idleBeatResultSB.toString());
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteConsistentHash.java
similarity index 62%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteConsistentHash.java
index 41ac671c..2461da5a 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteConsistentHash.java
@@ -1,30 +1,32 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
-import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
-import java.util.SortedMap;
+import java.util.Map;
import java.util.TreeMap;
/**
* 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;
* a、virtual node:解决不均衡问题
* b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围
+ *
* Created by xuxueli on 17/3/10.
*/
public class ExecutorRouteConsistentHash extends ExecutorRouter {
- private static int VIRTUAL_NODE_NUM = 100;
+ private static final int VIRTUAL_NODE_NUM = 100;
/**
* get hash code on 2^32 ring (md5散列的方式计算hash值)
- * @param key
- * @return
+ *
+ * @param key key
+ * @return hash code
*/
private static long hash(String key) {
@@ -37,11 +39,7 @@ public class ExecutorRouteConsistentHash extends ExecutorRouter {
}
md5.reset();
byte[] keyBytes = null;
- try {
- keyBytes = key.getBytes("UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException("Unknown string :" + key, e);
- }
+ keyBytes = key.getBytes(StandardCharsets.UTF_8);
md5.update(keyBytes);
byte[] digest = md5.digest();
@@ -52,15 +50,22 @@ public class ExecutorRouteConsistentHash extends ExecutorRouter {
| ((long) (digest[1] & 0xFF) << 8)
| (digest[0] & 0xFF);
- long truncateHashCode = hashCode & 0xffffffffL;
- return truncateHashCode;
+ return hashCode & 0xffffffffL;
}
+ /**
+ * get address by jobId
+ *
+ * @param jobId job id
+ * @param addressList address list
+ * @return address
+ */
public String hashJob(int jobId, List addressList) {
+ // 1、hash ring
// ------A1------A2-------A3------
// -----------J1------------------
- TreeMap addressRing = new TreeMap();
+ TreeMap addressRing = new TreeMap<>();
for (String address: addressList) {
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
long addressHash = hash("SHARD-" + address + "-NODE-" + i);
@@ -68,18 +73,27 @@ public class ExecutorRouteConsistentHash extends ExecutorRouter {
}
}
+ // 2、generate job-hash
long jobHash = hash(String.valueOf(jobId));
- SortedMap lastRing = addressRing.tailMap(jobHash);
+
+ // 3、route job-node
+ Map.Entry ceilingEntry = addressRing.ceilingEntry(jobHash);
+ if (ceilingEntry != null) {
+ return ceilingEntry.getValue();
+ }
+ /*SortedMap lastRing = addressRing.tailMap(jobHash);
if (!lastRing.isEmpty()) {
return lastRing.get(lastRing.firstKey());
- }
+ }*/
+
+ // 4、default first node
return addressRing.firstEntry().getValue();
}
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
String address = hashJob(triggerParam.getJobId(), addressList);
- return new ReturnT(address);
+ return Response.ofSuccess(address);
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFailover.java
similarity index 53%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFailover.java
index a2e4c909..c235a17c 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFailover.java
@@ -1,11 +1,11 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.admin.core.util.I18nUtil;
-import com.xxl.job.core.biz.ExecutorBiz;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.admin.framework.util.I18nUtil;
+import com.xxl.job.core.openapi.ExecutorBiz;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import java.util.List;
@@ -15,18 +15,18 @@ import java.util.List;
public class ExecutorRouteFailover extends ExecutorRouter {
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
StringBuffer beatResultSB = new StringBuffer();
for (String address : addressList) {
// beat
- ReturnT beatResult = null;
+ Response beatResult = null;
try {
- ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
+ ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address);
beatResult = executorBiz.beat();
} catch (Exception e) {
logger.error(e.getMessage(), e);
- beatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e );
+ beatResult = Response.ofFail(e.getMessage() );
}
beatResultSB.append( (beatResultSB.length()>0)?"
":"")
.append(I18nUtil.getString("jobconf_beat") + ":")
@@ -35,14 +35,14 @@ public class ExecutorRouteFailover extends ExecutorRouter {
.append("
msg:").append(beatResult.getMsg());
// beat success
- if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
+ if (beatResult.isSuccess()) {
beatResult.setMsg(beatResultSB.toString());
- beatResult.setContent(address);
+ beatResult.setData(address);
return beatResult;
}
}
- return new ReturnT(ReturnT.FAIL_CODE, beatResultSB.toString());
+ return Response.ofFail( beatResultSB.toString());
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFirst.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFirst.java
new file mode 100644
index 00000000..01723582
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteFirst.java
@@ -0,0 +1,19 @@
+package com.xxl.job.admin.business.scheduler.route.strategy;
+
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteFirst extends ExecutorRouter {
+
+ @Override
+ public Response route(TriggerRequest triggerParam, List addressList){
+ return Response.ofSuccess(addressList.get(0));
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLFU.java
similarity index 73%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLFU.java
index 9df19726..b9aca631 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLFU.java
@@ -1,8 +1,8 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -17,6 +17,11 @@ import java.util.concurrent.ConcurrentMap;
*/
public class ExecutorRouteLFU extends ExecutorRouter {
+ /**
+ * job lfu map
+ *
+ * >
+ */
private static ConcurrentMap> jobLfuMap = new ConcurrentHashMap>();
private static long CACHE_VALID_TIME = 0;
@@ -31,7 +36,7 @@ public class ExecutorRouteLFU extends ExecutorRouter {
// lfu item init
HashMap lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;
if (lfuItemMap == null) {
- lfuItemMap = new HashMap();
+ lfuItemMap = new HashMap<>();
jobLfuMap.putIfAbsent(jobId, lfuItemMap); // 避免重复覆盖
}
@@ -48,32 +53,26 @@ public class ExecutorRouteLFU extends ExecutorRouter {
delKeys.add(existKey);
}
}
- if (delKeys.size() > 0) {
+ if (!delKeys.isEmpty()) {
for (String delKey: delKeys) {
lfuItemMap.remove(delKey);
}
}
// load least userd count address
- List> lfuItemList = new ArrayList>(lfuItemMap.entrySet());
- Collections.sort(lfuItemList, new Comparator>() {
- @Override
- public int compare(Map.Entry o1, Map.Entry o2) {
- return o1.getValue().compareTo(o2.getValue());
- }
- });
+ List> lfuItemList = new ArrayList<>(lfuItemMap.entrySet());
+ lfuItemList.sort(Map.Entry.comparingByValue()); // 默认升序, 获取 Value 最小值
Map.Entry addressItem = lfuItemList.get(0);
- String minAddress = addressItem.getKey();
addressItem.setValue(addressItem.getValue() + 1);
return addressItem.getKey();
}
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
String address = route(triggerParam.getJobId(), addressList);
- return new ReturnT(address);
+ return Response.ofSuccess(address);
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLRU.java
similarity index 78%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLRU.java
index 2d540067..37eda780 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLRU.java
@@ -1,8 +1,8 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -19,6 +19,11 @@ import java.util.concurrent.ConcurrentMap;
*/
public class ExecutorRouteLRU extends ExecutorRouter {
+ /**
+ * job lru map
+ *
+ * >
+ */
private static ConcurrentMap> jobLRUMap = new ConcurrentHashMap>();
private static long CACHE_VALID_TIME = 0;
@@ -38,7 +43,7 @@ public class ExecutorRouteLRU extends ExecutorRouter {
* a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;
* b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;
*/
- lruItem = new LinkedHashMap(16, 0.75f, true);
+ lruItem = new LinkedHashMap<>(16, 0.75f, true);
jobLRUMap.putIfAbsent(jobId, lruItem);
}
@@ -55,22 +60,21 @@ public class ExecutorRouteLRU extends ExecutorRouter {
delKeys.add(existKey);
}
}
- if (delKeys.size() > 0) {
+ if (!delKeys.isEmpty()) {
for (String delKey: delKeys) {
lruItem.remove(delKey);
}
}
- // load
+ // load first elment, eldest entry
String eldestKey = lruItem.entrySet().iterator().next().getKey();
- String eldestValue = lruItem.get(eldestKey);
- return eldestValue;
+ return lruItem.get(eldestKey);
}
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
String address = route(triggerParam.getJobId(), addressList);
- return new ReturnT(address);
+ return Response.ofSuccess(address);
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLast.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLast.java
new file mode 100644
index 00000000..aa8840ff
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteLast.java
@@ -0,0 +1,19 @@
+package com.xxl.job.admin.business.scheduler.route.strategy;
+
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteLast extends ExecutorRouter {
+
+ @Override
+ public Response route(TriggerRequest triggerParam, List addressList) {
+ return Response.ofSuccess(addressList.get(addressList.size()-1));
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRandom.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRandom.java
new file mode 100644
index 00000000..a04b24bc
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRandom.java
@@ -0,0 +1,23 @@
+package com.xxl.job.admin.business.scheduler.route.strategy;
+
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteRandom extends ExecutorRouter {
+
+ private static Random localRandom = new Random();
+
+ @Override
+ public Response route(TriggerRequest triggerParam, List addressList) {
+ String address = addressList.get(localRandom.nextInt(addressList.size()));
+ return Response.ofSuccess(address);
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRound.java
similarity index 77%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRound.java
index d0ea2baa..0666ea81 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRound.java
@@ -1,8 +1,8 @@
-package com.xxl.job.admin.core.route.strategy;
+package com.xxl.job.admin.business.scheduler.route.strategy;
-import com.xxl.job.admin.core.route.ExecutorRouter;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouter;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.response.Response;
import java.util.List;
import java.util.Random;
@@ -38,9 +38,9 @@ public class ExecutorRouteRound extends ExecutorRouter {
}
@Override
- public ReturnT route(TriggerParam triggerParam, List addressList) {
+ public Response route(TriggerRequest triggerParam, List addressList) {
String address = addressList.get(count(triggerParam.getJobId())%addressList.size());
- return new ReturnT(address);
+ return Response.ofSuccess(address);
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobCompleteHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobCompleteHelper.java
new file mode 100644
index 00000000..4cdbd340
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobCompleteHelper.java
@@ -0,0 +1,153 @@
+package com.xxl.job.admin.business.scheduler.thread;
+
+import com.xxl.job.admin.business.model.XxlJobLog;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.framework.util.I18nUtil;
+import com.xxl.job.core.context.XxlJobContext;
+import com.xxl.job.core.openapi.model.CallbackRequest;
+import com.xxl.tool.concurrent.CyclicThread;
+import com.xxl.tool.core.DateTool;
+import com.xxl.tool.response.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * job complate, for callback and result-lost
+ *
+ * @author xuxueli 2015-9-1 18:05:56
+ */
+public class JobCompleteHelper {
+ private static final Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class);
+
+ // ---------------------- monitor ----------------------
+
+ private ThreadPoolExecutor callbackThreadPool = null;
+ private CyclicThread jobMonitorThread;
+
+ /**
+ * start
+ */
+ public void start(){
+
+ // 1、callbackThreadPool
+ callbackThreadPool = new ThreadPoolExecutor(
+ 2,
+ 20,
+ 30L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue(3000),
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode());
+ }
+ },
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ r.run();
+ logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now).");
+ }
+ });
+
+
+ // 2、jobMonitorThread
+ jobMonitorThread = new CyclicThread("JobCompleteHelper#jobMonitorThread", true, new Runnable() {
+ @Override
+ public void run() {
+ // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
+ Date losedTime = DateTool.addMinutes(new Date(), -10);
+ List losedJobIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findLostJobIds(losedTime);
+
+ if (losedJobIds!=null && losedJobIds.size()>0) {
+ for (Long logId: losedJobIds) {
+
+ XxlJobLog jobLog = new XxlJobLog();
+ jobLog.setId(logId);
+
+ jobLog.setHandleTime(new Date());
+ jobLog.setHandleCode(XxlJobContext.HANDLE_CODE_FAIL);
+ jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") );
+
+ XxlJobAdminBootstrap.getInstance().getJobCompleter().complete(jobLog);
+ }
+
+ }
+ }
+ }, 60 * 1000L, true);
+ jobMonitorThread.start();
+
+ }
+
+ /**
+ * stop
+ */
+ public void stop(){
+
+ // 1、callbackThreadPool
+ callbackThreadPool.shutdownNow();
+
+ // 2、jobMonitorThread
+ jobMonitorThread.stop();
+ }
+
+
+ // ---------------------- helper ----------------------
+
+ /**
+ * callback
+ *
+ * @param callbackParamList callback param
+ * @return callback result
+ */
+ public Response callback(List callbackParamList) {
+
+ callbackThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ for (CallbackRequest callbackRequest: callbackParamList) {
+ Response callbackResult = doCallback(callbackRequest);
+ logger.debug(">>>>>>>>> JobApiController.callback {}, callbackRequest={}, callbackResult={}",
+ (callbackResult.isSuccess()?"success":"fail"), callbackRequest, callbackResult);
+ }
+ }
+ });
+
+ return Response.ofSuccess();
+ }
+
+ private Response doCallback(CallbackRequest handleCallbackParam) {
+ // valid log item
+ XxlJobLog log = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().load(handleCallbackParam.getLogId());
+ if (log == null) {
+ return Response.ofFail( "log item not found.");
+ }
+ if (log.getHandleCode() > 0) {
+ return Response.ofFail("log repeate callback."); // avoid repeat callback, trigger child job etc
+ }
+
+ // handle msg
+ StringBuffer handleMsg = new StringBuffer();
+ if (log.getHandleMsg()!=null) {
+ handleMsg.append(log.getHandleMsg()).append("
");
+ }
+ if (handleCallbackParam.getHandleMsg() != null) {
+ handleMsg.append(handleCallbackParam.getHandleMsg());
+ }
+
+ // success, save log
+ log.setHandleTime(new Date());
+ log.setHandleCode(handleCallbackParam.getHandleCode());
+ log.setHandleMsg(handleMsg.toString());
+ XxlJobAdminBootstrap.getInstance().getJobCompleter().complete(log);
+
+ return Response.ofSuccess();
+ }
+
+
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobFailAlarmMonitorHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobFailAlarmMonitorHelper.java
new file mode 100644
index 00000000..34161b75
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobFailAlarmMonitorHelper.java
@@ -0,0 +1,82 @@
+package com.xxl.job.admin.business.scheduler.thread;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.model.XxlJobLog;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.trigger.TriggerTypeEnum;
+import com.xxl.job.admin.framework.util.I18nUtil;
+import com.xxl.tool.concurrent.CyclicThread;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * job fail-monitor helper
+ *
+ * @author xuxueli 2015-9-1 18:05:56
+ */
+public class JobFailAlarmMonitorHelper {
+ private static Logger logger = LoggerFactory.getLogger(JobFailAlarmMonitorHelper.class);
+
+
+ // ---------------------- monitor ----------------------
+
+ /**
+ * monitor thread
+ */
+ private CyclicThread monitorThread;
+
+ /**
+ * start
+ */
+ public void start(){
+
+ monitorThread = new CyclicThread("JobFailAlarmMonitorHelper#monitorThread", true, new Runnable() {
+ @Override
+ public void run() {
+ List failLogIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findFailJobLogIds(1000);
+ if (failLogIds!=null && !failLogIds.isEmpty()) {
+ for (long failLogId: failLogIds) {
+
+ // lock log
+ int lockRet = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateAlarmStatus(failLogId, 0, -1);
+ if (lockRet < 1) {
+ continue;
+ }
+ XxlJobLog log = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().load(failLogId);
+ XxlJobInfo info = XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().loadById(log.getJobId());
+
+ // 1、fail retry monitor
+ if (log.getExecutorFailRetryCount() > 0) {
+ XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
+ String retryMsg = "
>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
";
+ log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
+ XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateTriggerInfo(log);
+ }
+
+ // 2、fail alarm monitor
+ int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
+ if (info != null) {
+ boolean alarmResult = XxlJobAdminBootstrap.getInstance().getJobAlarmer().alarm(info, log);
+ newAlarmStatus = alarmResult?2:3;
+ } else {
+ newAlarmStatus = 1;
+ }
+
+ XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateAlarmStatus(failLogId, -1, newAlarmStatus);
+ }
+ }
+ }
+ }, 10 * 1000L, true);
+ monitorThread.start();
+ }
+
+ /**
+ * stop
+ */
+ public void stop(){
+ monitorThread.stop();
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobLogReportHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobLogReportHelper.java
new file mode 100644
index 00000000..181a70a1
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobLogReportHelper.java
@@ -0,0 +1,128 @@
+package com.xxl.job.admin.business.scheduler.thread;
+
+import com.xxl.job.admin.business.model.XxlJobLogReport;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.tool.concurrent.CyclicThread;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * job log report helper
+ *
+ * @author xuxueli 2019-11-22
+ */
+public class JobLogReportHelper {
+ private static final Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class);
+
+ private CyclicThread logReportThread;
+ private AtomicLong lastCleanLogTime;
+
+ /**
+ * start
+ */
+ public void start(){
+
+ /**
+ * last clean log time ( Thread-safe concurrent reading and writing )
+ */
+ lastCleanLogTime = new AtomicLong(0);
+
+ // log report thread
+ logReportThread = new CyclicThread("JobLogReportHelper#logReportThread", true, new Runnable() {
+ @Override
+ public void run() {
+
+ // 1、log-report refresh: refresh log report in 3 days
+ for (int i = 0; i < 3; i++) {
+
+ // today
+ Calendar itemDay = Calendar.getInstance();
+ itemDay.add(Calendar.DAY_OF_MONTH, -i);
+ itemDay.set(Calendar.HOUR_OF_DAY, 0);
+ itemDay.set(Calendar.MINUTE, 0);
+ itemDay.set(Calendar.SECOND, 0);
+ itemDay.set(Calendar.MILLISECOND, 0);
+
+ Date todayFrom = itemDay.getTime();
+
+ itemDay.set(Calendar.HOUR_OF_DAY, 23);
+ itemDay.set(Calendar.MINUTE, 59);
+ itemDay.set(Calendar.SECOND, 59);
+ itemDay.set(Calendar.MILLISECOND, 999);
+
+ Date todayTo = itemDay.getTime();
+
+ // refresh log-report every minute
+ XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
+ xxlJobLogReport.setTriggerDay(todayFrom);
+ xxlJobLogReport.setRunningCount(0);
+ xxlJobLogReport.setSucCount(0);
+ xxlJobLogReport.setFailCount(0);
+ xxlJobLogReport.setUpdateTime(new Date());
+
+ // fill count-data
+ Map triggerCountMap = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findLogReport(todayFrom, todayTo);
+ if (triggerCountMap!=null && !triggerCountMap.isEmpty()) {
+ int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCount"))):0;
+ int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0;
+ int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0;
+ int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;
+
+ xxlJobLogReport.setRunningCount(triggerDayCountRunning);
+ xxlJobLogReport.setSucCount(triggerDayCountSuc);
+ xxlJobLogReport.setFailCount(triggerDayCountFail);
+ }
+
+ // do refresh:
+ XxlJobAdminBootstrap.getInstance().getXxlJobLogReportMapper().saveOrUpdate(xxlJobLogReport); // 0-fail; 1-save suc; 2-update suc;
+ /*if (ret < 1) {
+ XxlJobAdminBootstrap.getInstance().getXxlJobLogReportMapper().save(xxlJobLogReport);
+ }*/
+ }
+
+ // 2、log-clean: switch open & once each day
+ if (XxlJobAdminBootstrap.getInstance().getLogretentiondays()>0
+ && System.currentTimeMillis() - lastCleanLogTime.longValue() > 24*60*60*1000) {
+
+ // expire-time
+ Calendar expiredDay = Calendar.getInstance();
+ expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminBootstrap.getInstance().getLogretentiondays());
+ expiredDay.set(Calendar.HOUR_OF_DAY, 0);
+ expiredDay.set(Calendar.MINUTE, 0);
+ expiredDay.set(Calendar.SECOND, 0);
+ expiredDay.set(Calendar.MILLISECOND, 0);
+ Date clearBeforeTime = expiredDay.getTime();
+
+ // clean expired log
+ List logIds = null;
+ do {
+ logIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
+ if (logIds!=null && !logIds.isEmpty()) {
+ XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().clearLog(logIds);
+ }
+ } while (logIds!=null && !logIds.isEmpty());
+
+ // update clean time
+ lastCleanLogTime.set(System.currentTimeMillis());
+ }
+
+ }
+ }, 60 * 1000L, true);
+ logReportThread.start();
+
+ }
+
+ /**
+ * stop
+ */
+ public void stop(){
+ logReportThread.stop();
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobRegistryHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobRegistryHelper.java
new file mode 100644
index 00000000..4d43a5a4
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobRegistryHelper.java
@@ -0,0 +1,204 @@
+package com.xxl.job.admin.business.scheduler.thread;
+
+import com.xxl.job.admin.business.model.XxlJobGroup;
+import com.xxl.job.admin.business.model.XxlJobRegistry;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.core.constant.Const;
+import com.xxl.job.core.constant.RegistType;
+import com.xxl.job.core.openapi.model.RegistryRequest;
+import com.xxl.tool.concurrent.CyclicThread;
+import com.xxl.tool.core.StringTool;
+import com.xxl.tool.response.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * job registry instance helper
+ *
+ * @author xuxueli 2016-10-02 19:10:24
+ */
+public class JobRegistryHelper {
+ private static final Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class);
+
+
+ /**
+ * registry or remove thread pool
+ */
+ private ThreadPoolExecutor registryOrRemoveThreadPool = null;
+
+ /**
+ * registry monitor thread
+ */
+ private CyclicThread registryMonitorThread;
+
+ /**
+ * start
+ */
+ public void start(){
+
+ // 1、for registry or remove
+ registryOrRemoveThreadPool = new ThreadPoolExecutor(
+ 2,
+ 10,
+ 30L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue(2000),
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
+ }
+ },
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ r.run();
+ logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
+ }
+ });
+
+ // 2、for registry monitor
+ registryMonitorThread = new CyclicThread("JobRegistryHelper#registryMonitorThread", true, new Runnable() {
+ @Override
+ public void run() {
+ // auto registry group
+ List groupList = XxlJobAdminBootstrap.getInstance().getXxlJobGroupMapper().findByAddressType(0);
+ if (groupList!=null && !groupList.isEmpty()) {
+
+ // remove dead address (admin/executor)
+ List ids = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().findDead(Const.DEAD_TIMEOUT, new Date());
+ if (ids!=null && !ids.isEmpty()) {
+ XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().removeDead(ids);
+ }
+
+ // fresh online address (admin/executor)
+ HashMap> appAddressMap = new HashMap>();
+ List list = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().findAll(Const.DEAD_TIMEOUT, new Date());
+ if (list != null) {
+ for (XxlJobRegistry item: list) {
+ if (RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
+ String appname = item.getRegistryKey();
+ List registryList = appAddressMap.get(appname);
+ if (registryList == null) {
+ registryList = new ArrayList();
+ }
+
+ if (!registryList.contains(item.getRegistryValue())) {
+ registryList.add(item.getRegistryValue());
+ }
+ appAddressMap.put(appname, registryList);
+ }
+ }
+ }
+
+ // fresh group address
+ for (XxlJobGroup group: groupList) {
+ List registryList = appAddressMap.get(group.getAppname());
+ String addressListStr = null;
+ if (registryList!=null && !registryList.isEmpty()) {
+ Collections.sort(registryList);
+ StringBuilder addressListSB = new StringBuilder();
+ for (String item:registryList) {
+ addressListSB.append(item).append(",");
+ }
+ addressListStr = addressListSB.toString();
+ addressListStr = addressListStr.substring(0, addressListStr.length()-1);
+ }
+ group.setAddressList(addressListStr);
+ group.setUpdateTime(new Date());
+
+ XxlJobAdminBootstrap.getInstance().getXxlJobGroupMapper().update(group);
+ }
+ }
+ }
+ }, Const.BEAT_TIMEOUT * 1000L, true);
+ registryMonitorThread.start();
+ }
+
+
+ /**
+ * stop
+ */
+ public void stop(){
+
+ // 1、registryOrRemoveThreadPool
+ registryOrRemoveThreadPool.shutdownNow();
+
+ // 2、registryMonitorThread
+ registryMonitorThread.stop();
+ }
+
+
+ // ---------------------- tool ----------------------
+
+ /**
+ * registry
+ */
+ public Response registry(RegistryRequest registryParam) {
+
+ // valid
+ if (StringTool.isBlank(registryParam.getRegistryGroup())
+ || StringTool.isBlank(registryParam.getRegistryKey())
+ || StringTool.isBlank(registryParam.getRegistryValue())) {
+ return Response.ofFail("Illegal Argument.");
+ }
+
+ // async execute
+ registryOrRemoveThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ // 0-fail; 1-save suc; 2-update suc;
+ int ret = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().registrySaveOrUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
+ if (ret == 1) {
+ // fresh (add)
+ freshGroupRegistryInfo(registryParam);
+ }
+ /*int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
+ if (ret < 1) {
+ XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
+
+ // fresh
+ freshGroupRegistryInfo(registryParam);
+ }*/
+ }
+ });
+
+ return Response.ofSuccess();
+ }
+
+ /**
+ * registry remove
+ */
+ public Response registryRemove(RegistryRequest registryParam) {
+
+ // valid
+ if (StringTool.isBlank(registryParam.getRegistryGroup())
+ || StringTool.isBlank(registryParam.getRegistryKey())
+ || StringTool.isBlank(registryParam.getRegistryValue())) {
+ return Response.ofFail("Illegal Argument.");
+ }
+
+ // async execute
+ registryOrRemoveThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ int ret = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
+ if (ret > 0) {
+ // fresh (delete)
+ freshGroupRegistryInfo(registryParam);
+ }
+ }
+ });
+
+ return Response.ofSuccess();
+ }
+
+ private void freshGroupRegistryInfo(RegistryRequest registryParam){
+ // Under consideration, prevent affecting core tables
+ }
+
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobScheduleHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobScheduleHelper.java
new file mode 100644
index 00000000..e60c531f
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobScheduleHelper.java
@@ -0,0 +1,374 @@
+package com.xxl.job.admin.business.scheduler.thread;
+
+import com.xxl.job.admin.business.constant.TriggerStatus;
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.misfire.MisfireStrategyEnum;
+import com.xxl.job.admin.business.scheduler.trigger.TriggerTypeEnum;
+import com.xxl.job.admin.business.scheduler.type.ScheduleTypeEnum;
+import com.xxl.tool.core.CollectionTool;
+import com.xxl.tool.core.MapTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author xuxueli 2019-05-21
+ */
+public class JobScheduleHelper {
+ private static final Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class);
+
+
+ /**
+ * pre-read time for scheduler, increase efficiency
+ */
+ public static final long PRE_READ_MS = 5000;
+ /*
+ * elegant shutdown wait seconds
+ */
+ private static final long ELEGANT_SHUTDOWN_WAITING_SECONDS = 10;
+
+ private Thread scheduleThread;
+ private Thread ringThread;
+ private volatile boolean scheduleThreadToStop = false;
+ private volatile boolean ringThreadToStop = false;
+ private final Map> ringData = new ConcurrentHashMap<>();
+
+ /**
+ * start
+ */
+ public void start(){
+
+ // init thread flag
+ scheduleThreadToStop = false;
+ ringThreadToStop = false;
+
+ // 1、schedule thread
+ scheduleThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+
+ // align time
+ try {
+ TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
+ } catch (Throwable e) {
+ if (!scheduleThreadToStop) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+ logger.info(">>>>>>>>> init xxl-job admin scheduler success.");
+
+ // pre-read count: treadpool-size * 10 (trigger-qps: 1000ms / 100ms each trigger cost)
+ int preReadCount = (XxlJobAdminBootstrap.getInstance().getTriggerPoolFastMax() + XxlJobAdminBootstrap.getInstance().getTriggerPoolSlowMax()) * 10;
+
+ // do schedule
+ while (!scheduleThreadToStop) {
+
+ // param
+ long start = System.currentTimeMillis();
+ boolean preReadSuc = true;
+
+ // transaction start
+ TransactionStatus transactionStatus = null;
+ try {
+ transactionStatus = XxlJobAdminBootstrap.getInstance().getTransactionManager().getTransaction(new DefaultTransactionDefinition());
+ // 1、job lock
+ String lockedRecord = XxlJobAdminBootstrap.getInstance().getXxlJobLockMapper().scheduleLock();
+ long nowTime = System.currentTimeMillis();
+
+ // scan and process job
+ List scheduleList = XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
+ if (CollectionTool.isNotEmpty(scheduleList)) {
+
+ // 2、push time-ring
+ for (XxlJobInfo jobInfo: scheduleList) {
+
+ // time-ring jump
+ if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
+ // 2.1、trigger-expire > 5s:pass && make next-trigger-time
+
+ // 1、misfire handle
+ MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
+ misfireStrategyEnum.getMisfireHandler().handle(jobInfo.getId());
+
+ // 2、fresh next
+ refreshNextTriggerTime(jobInfo, new Date());
+
+ } else if (nowTime >= jobInfo.getTriggerNextTime()) {
+ // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time
+
+ // 1、trigger direct
+ XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
+ logger.debug(">>>>>>>>>>> xxl-job, schedule expire, direct trigger : jobId = " + jobInfo.getId() );
+
+ // 2、fresh next
+ refreshNextTriggerTime(jobInfo, new Date());
+
+ // next-trigger-time in 5s, pre-read again
+ if (jobInfo.getTriggerStatus()== TriggerStatus.RUNNING.getValue() && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
+
+ // 1、make ring second
+ int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
+
+ // 2、push time ring (pre read)
+ pushTimeRing(ringSecond, jobInfo.getId());
+ logger.debug(">>>>>>>>>>> xxl-job, schedule pre-read, push trigger : jobId = " + jobInfo.getId() );
+
+ // 3、fresh next
+ refreshNextTriggerTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
+
+ }
+
+ } else {
+ // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time
+
+ // 1、make ring second
+ int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
+
+ // 2、push time ring
+ pushTimeRing(ringSecond, jobInfo.getId());
+ logger.debug(">>>>>>>>>>> xxl-job, schedule normal, push trigger : jobId = " + jobInfo.getId() );
+
+ // 3、fresh next
+ refreshNextTriggerTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
+
+ }
+
+ }
+
+ // 3、update trigger info
+ /*for (XxlJobInfo jobInfo: scheduleList) {
+ XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().scheduleUpdate(jobInfo);
+ }*/
+ int batchSize = XxlJobAdminBootstrap.getInstance().getScheduleBatchSize();
+ List> scheduleListBatches = CollectionTool.split(scheduleList, batchSize);
+ for (List scheduleListBatch : scheduleListBatches) {
+ int totalAffected = XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().scheduleBatchUpdate(scheduleListBatch);
+ logger.debug(">>>>>>>>>>> xxl-job, JobScheduleHelper scheduleBatchUpdate records:" + totalAffected);
+ }
+
+ } else {
+ preReadSuc = false;
+ }
+
+ } catch (Throwable e) {
+ if (!scheduleThreadToStop) {
+ logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e.getMessage(), e);
+ }
+ } finally {
+ // transaction commit
+ try {
+ if (transactionStatus != null) {
+ XxlJobAdminBootstrap.getInstance().getTransactionManager().commit(transactionStatus); // avlid schedule repeat
+ }
+ } catch (Throwable e) {
+ logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread transaction commit error:{}", e.getMessage(), e);
+ }
+ }
+ // transaction end
+ long cost = System.currentTimeMillis()-start;
+
+
+ // Wait seconds, align second
+ if (cost < 1000) { // scan-overtime, not wait
+ try {
+ // pre-read period: success > scan each second; fail > skip this period;
+ TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
+ } catch (Throwable e) {
+ if (!scheduleThreadToStop) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ }
+
+ logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
+ }
+ });
+ scheduleThread.setDaemon(true);
+ scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
+ scheduleThread.start();
+
+ // 2、ring thread
+ ringThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+
+ while (!ringThreadToStop) {
+
+ // align second
+ try {
+ TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
+ } catch (Throwable e) {
+ if (!ringThreadToStop) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ try {
+ // second data
+ List ringItemData = new ArrayList<>();
+
+ // collect rind data, by second
+ int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
+ for (int i = 0; i <= 2; i++) { // 避免调度遗漏:处理耗时太长、跨过刻度,除当前刻度外 + 向前校验2个刻度;
+ List ringItemList = ringData.remove( (nowSecond+60-i)%60 );
+ if (CollectionTool.isNotEmpty(ringItemList)) {
+ // distinct for each second
+ List ringItemListDistinct = ringItemList.stream().distinct().toList(); // 避免调度重复:重复推送时间轮刻度,去重只保留一个;;
+ if (ringItemListDistinct.size() < ringItemList.size()) {
+ logger.warn(">>>>>>>>>>> xxl-job, time-ring found job repeat beat : " + nowSecond + " = " + ringItemData);
+ }
+
+ // collect ring item
+ ringItemData.addAll(ringItemListDistinct);
+ }
+ }
+
+ // ring trigger
+ logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + ringItemData);
+ if (CollectionTool.isNotEmpty(ringItemData)) {
+ // do trigger
+ for (int jobId: ringItemData) {
+ // do trigger
+ XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
+ }
+ // clear
+ ringItemData.clear();
+ }
+ } catch (Throwable e) {
+ if (!ringThreadToStop) {
+ logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e.getMessage(), e);
+ }
+ }
+ }
+ logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
+ }
+ });
+ ringThread.setDaemon(true);
+ ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
+ ringThread.start();
+ }
+
+ /**
+ * refresh next trigger time of job
+ *
+ * @param jobInfo job info
+ * @param fromTime from time
+ */
+ private void refreshNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) {
+ try {
+ // generate next trigger time
+ ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), ScheduleTypeEnum.NONE);
+ Date nextTriggerTime = scheduleTypeEnum.getScheduleType().generateNextTriggerTime(jobInfo, fromTime);
+
+ // refresh next trigger-time + status
+ if (nextTriggerTime != null) {
+ // generate success
+ jobInfo.setTriggerStatus(-1); // pass, may be Inaccurate
+ jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
+ jobInfo.setTriggerNextTime(nextTriggerTime.getTime());
+ } else {
+ // generate fail, stop job
+ jobInfo.setTriggerStatus(TriggerStatus.STOPPED.getValue());
+ jobInfo.setTriggerLastTime(0);
+ jobInfo.setTriggerNextTime(0);
+ logger.error(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}",
+ jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf());
+ }
+ } catch (Throwable e) {
+ // generate error, stop job
+ jobInfo.setTriggerStatus(TriggerStatus.STOPPED.getValue());
+ jobInfo.setTriggerLastTime(0);
+ jobInfo.setTriggerNextTime(0);
+
+ logger.error(">>>>>>>>>>> xxl-job, refreshNextValidTime error for job: jobId={}, scheduleType={}, scheduleConf={}",
+ jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf(), e);
+ }
+ }
+
+ /**
+ * push time ring
+ *
+ * @param ringSecond ring second
+ * @param jobId job id
+ */
+ private void pushTimeRing(int ringSecond, int jobId){
+ // get ringItemData, init when not exists
+ List ringItemList = ringData.computeIfAbsent(
+ ringSecond,
+ k -> new ArrayList<>());
+
+ // push async rind
+ ringItemList.add(jobId);
+ logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + List.of(ringItemList));
+ }
+
+ /**
+ * stop
+ */
+ public void stop(){
+
+ // 1、stop schedule
+ scheduleThreadToStop = true;
+ try {
+ TimeUnit.SECONDS.sleep(1); // wait
+ } catch (Throwable e) {
+ logger.error(e.getMessage(), e);
+ }
+ if (scheduleThread.getState() != Thread.State.TERMINATED){
+ // interrupt and wait
+ scheduleThread.interrupt();
+ try {
+ scheduleThread.join();
+ } catch (Throwable e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ // if has ring data, wait for elegent shutdown
+ boolean hasRingData = false;
+ if (MapTool.isNotEmpty(ringData)) {
+ for (int second : ringData.keySet()) {
+ List ringItemList = ringData.get(second);
+ if (CollectionTool.isNotEmpty(ringItemList)) {
+ hasRingData = true;
+ break;
+ }
+ }
+ }
+ if (hasRingData) {
+ try {
+ TimeUnit.SECONDS.sleep(ELEGANT_SHUTDOWN_WAITING_SECONDS);
+ } catch (Throwable e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ // 2、stop ring (wait job-in-memory stop)
+ ringThreadToStop = true;
+ try {
+ TimeUnit.SECONDS.sleep(1);
+ } catch (Throwable e) {
+ logger.error(e.getMessage(), e);
+ }
+ if (ringThread.getState() != Thread.State.TERMINATED){
+ // interrupt and wait
+ ringThread.interrupt();
+ try {
+ ringThread.join();
+ } catch (Throwable e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop");
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobTriggerPoolHelper.java
similarity index 64%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobTriggerPoolHelper.java
index 398713dd..e32d5b26 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobTriggerPoolHelper.java
@@ -1,8 +1,7 @@
-package com.xxl.job.admin.core.thread;
+package com.xxl.job.admin.business.scheduler.thread;
-import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
-import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
-import com.xxl.job.admin.core.trigger.XxlJobTrigger;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.trigger.TriggerTypeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,7 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* @author xuxueli 2018-07-03 21:08:07
*/
public class JobTriggerPoolHelper {
- private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class);
+ private static final Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class);
// ---------------------- trigger pool ----------------------
@@ -24,35 +23,52 @@ public class JobTriggerPoolHelper {
private ThreadPoolExecutor fastTriggerPool = null;
private ThreadPoolExecutor slowTriggerPool = null;
+ /**
+ * start
+ */
public void start(){
fastTriggerPool = new ThreadPoolExecutor(
10,
- XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(),
+ XxlJobAdminBootstrap.getInstance().getTriggerPoolFastMax(),
60L,
TimeUnit.SECONDS,
- new LinkedBlockingQueue(1000),
+ new LinkedBlockingQueue(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode());
}
+ },
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ logger.error(">>>>>>>>>>> xxl-job, admin JobTriggerPoolHelper-fastTriggerPool execute too fast, Runnable="+r.toString() );
+ }
});
slowTriggerPool = new ThreadPoolExecutor(
10,
- XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(),
+ XxlJobAdminBootstrap.getInstance().getTriggerPoolSlowMax(),
60L,
TimeUnit.SECONDS,
- new LinkedBlockingQueue(2000),
+ new LinkedBlockingQueue(5000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode());
}
+ },
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ logger.error(">>>>>>>>>>> xxl-job, admin JobTriggerPoolHelper-slowTriggerPool execute too fast, Runnable="+r.toString() );
+ }
});
}
-
+ /**
+ * stop
+ */
public void stop() {
//triggerPool.shutdown();
fastTriggerPool.shutdownNow();
@@ -66,15 +82,27 @@ public class JobTriggerPoolHelper {
private volatile ConcurrentMap jobTimeoutCountMap = new ConcurrentHashMap<>();
+ // ---------------------- tool ----------------------
+
/**
- * add trigger
+ * trigger job
+ *
+ * @param jobId
+ * @param triggerType
+ * @param failRetryCount
+ * >=0: use this param
+ * <0: use param from job info config
+ * @param executorShardingParam
+ * @param executorParam
+ * null: use job param
+ * not null: cover job param
*/
- public void addTrigger(final int jobId,
- final TriggerTypeEnum triggerType,
- final int failRetryCount,
- final String executorShardingParam,
- final String executorParam,
- final String addressList) {
+ public void trigger(final int jobId,
+ final TriggerTypeEnum triggerType,
+ final int failRetryCount,
+ final String executorShardingParam,
+ final String executorParam,
+ final String addressList) {
// choose thread pool
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
@@ -92,8 +120,8 @@ public class JobTriggerPoolHelper {
try {
// do trigger
- XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
- } catch (Exception e) {
+ XxlJobAdminBootstrap.getInstance().getJobTrigger().trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
+ } catch (Throwable e) {
logger.error(e.getMessage(), e);
} finally {
@@ -116,35 +144,11 @@ public class JobTriggerPoolHelper {
}
}
+ @Override
+ public String toString() {
+ return "Job Runnable, jobId:"+jobId;
+ }
});
}
-
-
- // ---------------------- helper ----------------------
-
- private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper();
-
- public static void toStart() {
- helper.start();
- }
- public static void toStop() {
- helper.stop();
- }
-
- /**
- * @param jobId
- * @param triggerType
- * @param failRetryCount
- * >=0: use this param
- * <0: use param from job info config
- * @param executorShardingParam
- * @param executorParam
- * null: use job param
- * not null: cover job param
- */
- public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) {
- helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
- }
-
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/JobTrigger.java
similarity index 53%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/JobTrigger.java
index 748befc6..3a027f9a 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/JobTrigger.java
@@ -1,29 +1,46 @@
-package com.xxl.job.admin.core.trigger;
-
-import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
-import com.xxl.job.admin.core.model.XxlJobGroup;
-import com.xxl.job.admin.core.model.XxlJobInfo;
-import com.xxl.job.admin.core.model.XxlJobLog;
-import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
-import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
-import com.xxl.job.admin.core.util.I18nUtil;
-import com.xxl.job.core.biz.ExecutorBiz;
-import com.xxl.job.core.biz.model.ReturnT;
-import com.xxl.job.core.biz.model.TriggerParam;
-import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
-import com.xxl.job.core.util.IpUtil;
-import com.xxl.job.core.util.ThrowableUtil;
+package com.xxl.job.admin.business.scheduler.trigger;
+
+import com.xxl.job.admin.business.mapper.XxlJobGroupMapper;
+import com.xxl.job.admin.business.mapper.XxlJobInfoMapper;
+import com.xxl.job.admin.business.mapper.XxlJobLogMapper;
+import com.xxl.job.admin.business.model.XxlJobGroup;
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.model.XxlJobLog;
+import com.xxl.job.admin.business.scheduler.config.XxlJobAdminBootstrap;
+import com.xxl.job.admin.business.scheduler.route.ExecutorRouteStrategyEnum;
+import com.xxl.job.admin.framework.util.I18nUtil;
+import com.xxl.job.core.constant.ExecutorBlockStrategyEnum;
+import com.xxl.job.core.context.XxlJobContext;
+import com.xxl.job.core.openapi.ExecutorBiz;
+import com.xxl.job.core.openapi.model.TriggerRequest;
+import com.xxl.tool.core.StringTool;
+import com.xxl.tool.error.ThrowableTool;
+import com.xxl.tool.http.IPTool;
+import com.xxl.tool.response.Response;
+import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
import java.util.Date;
/**
* xxl-job trigger
- * Created by xuxueli on 17/7/13.
+ *
+ * @author xuxueli 17/7/13.
*/
-public class XxlJobTrigger {
- private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class);
+@Component
+public class JobTrigger {
+ private static final Logger logger = LoggerFactory.getLogger(JobTrigger.class);
+
+
+ @Resource
+ private XxlJobInfoMapper xxlJobInfoMapper;
+ @Resource
+ private XxlJobGroupMapper xxlJobGroupMapper;
+ @Resource
+ private XxlJobLogMapper xxlJobLogMapper;
+
/**
* trigger job
@@ -34,6 +51,8 @@ public class XxlJobTrigger {
* >=0: use this param
* <0: use param from job info config
* @param executorShardingParam
+ * null: new sharding, all nodes
+ * not null: for retry, only one node
* @param executorParam
* null: use job param
* not null: cover job param
@@ -41,7 +60,7 @@ public class XxlJobTrigger {
* null: use executor addressList
* not null: cover
*/
- public static void trigger(int jobId,
+ public void trigger(int jobId,
TriggerTypeEnum triggerType,
int failRetryCount,
String executorShardingParam,
@@ -49,7 +68,7 @@ public class XxlJobTrigger {
String addressList) {
// load data
- XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
+ XxlJobInfo jobInfo = xxlJobInfoMapper.loadById(jobId);
if (jobInfo == null) {
logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
return;
@@ -58,57 +77,67 @@ public class XxlJobTrigger {
jobInfo.setExecutorParam(executorParam);
}
int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
- XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());
+ XxlJobGroup group = xxlJobGroupMapper.load(jobInfo.getJobGroup());
// cover addressList
- if (addressList!=null && addressList.trim().length()>0) {
+ if (StringTool.isNotBlank(addressList)) {
group.setAddressType(1);
group.setAddressList(addressList.trim());
}
// sharding param
int[] shardingParam = null;
+ Date triggerTime = new Date();
if (executorShardingParam!=null){
String[] shardingArr = executorShardingParam.split("/");
- if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
+ if (shardingArr.length==2 && StringTool.isNumeric(shardingArr[0]) && StringTool.isNumeric(shardingArr[1])) {
shardingParam = new int[2];
- shardingParam[0] = Integer.valueOf(shardingArr[0]);
- shardingParam[1] = Integer.valueOf(shardingArr[1]);
+ shardingParam[0] = Integer.parseInt(shardingArr[0]);
+ shardingParam[1] = Integer.parseInt(shardingArr[1]);
}
}
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
&& group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
&& shardingParam==null) {
for (int i = 0; i < group.getRegistryList().size(); i++) {
- processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
+ processTrigger(group, jobInfo, finalFailRetryCount, triggerType, triggerTime, i, group.getRegistryList().size());
}
} else {
if (shardingParam == null) {
shardingParam = new int[]{0, 1};
}
- processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
+ processTrigger(group, jobInfo, finalFailRetryCount, triggerType, triggerTime, shardingParam[0], shardingParam[1]);
}
}
- private static boolean isNumeric(String str){
+ /*private static boolean isNumeric(String str){
try {
int result = Integer.valueOf(str);
return true;
} catch (NumberFormatException e) {
return false;
}
- }
+ }*/
/**
+ * process trigger with log
+ *
* @param group job group, registry list may be empty
- * @param jobInfo
- * @param finalFailRetryCount
- * @param triggerType
+ * @param jobInfo job info
+ * @param finalFailRetryCount the fail-retry count
+ * @param triggerType trigger type
+ * @param triggerTime trigger time
* @param index sharding index
* @param total sharding index
*/
- private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){
+ private void processTrigger(XxlJobGroup group,
+ XxlJobInfo jobInfo,
+ int finalFailRetryCount,
+ TriggerTypeEnum triggerType,
+ Date triggerTime,
+ int index,
+ int total){
// param
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy
@@ -119,12 +148,12 @@ public class XxlJobTrigger {
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
- jobLog.setTriggerTime(new Date());
- XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog);
- logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());
+ jobLog.setTriggerTime(triggerTime);
+ xxlJobLogMapper.save(jobLog);
+ logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getJobId());
// 2、init trigger-param
- TriggerParam triggerParam = new TriggerParam();
+ TriggerRequest triggerParam = new TriggerRequest();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
@@ -140,7 +169,7 @@ public class XxlJobTrigger {
// 3、init address
String address = null;
- ReturnT routeAddressResult = null;
+ Response routeAddressResult = null;
if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
if (index < group.getRegistryList().size()) {
@@ -150,39 +179,60 @@ public class XxlJobTrigger {
}
} else {
routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
- if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
- address = routeAddressResult.getContent();
+ if (routeAddressResult.isSuccess()) {
+ address = routeAddressResult.getData();
}
}
} else {
- routeAddressResult = new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
+ routeAddressResult = Response.of(XxlJobContext.HANDLE_CODE_FAIL, I18nUtil.getString("jobconf_trigger_address_empty"));
}
// 4、trigger remote executor
- ReturnT triggerResult = null;
+ Response triggerResult = null;
if (address != null) {
- triggerResult = runExecutor(triggerParam, address);
+ triggerResult = doTrigger(triggerParam, address);
} else {
- triggerResult = new ReturnT(ReturnT.FAIL_CODE, null);
+ triggerResult = Response.of(XxlJobContext.HANDLE_CODE_FAIL, "Address Router Fail.");
}
// 5、collection trigger info
- StringBuffer triggerMsgSb = new StringBuffer();
+ // trigger config
+ StringBuilder triggerMsgSb = new StringBuilder();
triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle());
- triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
+ triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IPTool.getIp());
triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
.append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
if (shardingParam != null) {
- triggerMsgSb.append("("+shardingParam+")");
+ triggerMsgSb.append("(").append(shardingParam).append(")");
}
triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount);
- triggerMsgSb.append("
>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<<
")
- .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"
":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():"");
+ // trigger data
+ triggerMsgSb.append("
>>>>>>>>>>>").append(I18nUtil.getString("jobconf_trigger_run")).append("<<<<<<<<<<<
");
+ triggerMsgSb.append("
").append(I18nUtil.getString("joblog_field_executorAddress")).append(":");
+ if (StringTool.isNotBlank(address)) {
+ triggerMsgSb.append(address);
+ } else if (routeAddressResult!=null && !routeAddressResult.isSuccess() && routeAddressResult.getMsg()!=null) {
+ triggerMsgSb.append("address route fail, ").append(routeAddressResult.getMsg());
+ } else {
+ triggerMsgSb.append("address route fail.");
+ }
+ if (StringTool.isNotBlank(jobInfo.getExecutorHandler())) {
+ triggerMsgSb.append("
").append("JobHandler").append(":").append(jobInfo.getExecutorHandler());
+ }
+ triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorparam")).append(":").append(jobInfo.getExecutorParam());
+ triggerMsgSb.append("
").append(I18nUtil.getString("joblog_field_triggerMsg")).append(":");
+ if (triggerResult.isSuccess()) {
+ triggerMsgSb.append("success");
+ } else if (triggerResult.getMsg()!=null) {
+ triggerMsgSb.append("error, ").append(triggerResult.getMsg());
+ } else {
+ triggerMsgSb.append("fail");
+ }
// 6、save log trigger-info
jobLog.setExecutorAddress(address);
@@ -193,34 +243,39 @@ public class XxlJobTrigger {
//jobLog.setTriggerTime();
jobLog.setTriggerCode(triggerResult.getCode());
jobLog.setTriggerMsg(triggerMsgSb.toString());
- XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog);
+ xxlJobLogMapper.updateTriggerInfo(jobLog);
- logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
+ logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getJobId());
}
/**
- * run executor
- * @param triggerParam
- * @param address
- * @return
+ * do trigger with address
+ *
+ * @param triggerParam trigger param
+ * @param address the address
+ * @return return
*/
- public static ReturnT runExecutor(TriggerParam triggerParam, String address){
- ReturnT runResult = null;
+ private Response doTrigger(TriggerRequest triggerParam, String address){
try {
- ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
- runResult = executorBiz.run(triggerParam);
+ // build client
+ ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address);
+
+ // invoke
+ Response runResult = executorBiz.run(triggerParam);
+
+ // build result
+ StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
+ runResultSB.append("
address:").append(address);
+ runResultSB.append("
code:").append(runResult.getCode());
+ runResultSB.append("
msg:").append(runResult.getMsg());
+
+ // return
+ runResult.setMsg(runResultSB.toString());
+ return runResult;
} catch (Exception e) {
logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
- runResult = new ReturnT(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
+ return Response.of(XxlJobContext.HANDLE_CODE_FAIL, ThrowableTool.toString(e));
}
-
- StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
- runResultSB.append("
address:").append(address);
- runResultSB.append("
code:").append(runResult.getCode());
- runResultSB.append("
msg:").append(runResult.getMsg());
-
- runResult.setMsg(runResultSB.toString());
- return runResult;
}
}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/TriggerTypeEnum.java
similarity index 85%
rename from xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java
rename to xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/TriggerTypeEnum.java
index 446c90e9..61219f52 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/trigger/TriggerTypeEnum.java
@@ -1,6 +1,6 @@
-package com.xxl.job.admin.core.trigger;
+package com.xxl.job.admin.business.scheduler.trigger;
-import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.admin.framework.util.I18nUtil;
/**
* trigger type enum
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleType.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleType.java
new file mode 100644
index 00000000..b3f2f61d
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleType.java
@@ -0,0 +1,22 @@
+package com.xxl.job.admin.business.scheduler.type;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+
+import java.util.Date;
+
+/**
+ * Schedule Type
+ *
+ * @author xuxueli 2020-10-29
+ */
+public abstract class ScheduleType {
+
+ /**
+ * generate next trigger time
+ *
+ * @param jobInfo job info
+ * @param fromTime from time
+ */
+ public abstract Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception;
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleTypeEnum.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleTypeEnum.java
new file mode 100644
index 00000000..23ffeb2e
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/ScheduleTypeEnum.java
@@ -0,0 +1,62 @@
+package com.xxl.job.admin.business.scheduler.type;
+
+import com.xxl.job.admin.business.scheduler.type.strategy.CronScheduleType;
+import com.xxl.job.admin.business.scheduler.type.strategy.FixRateScheduleType;
+import com.xxl.job.admin.business.scheduler.type.strategy.NoneScheduleType;
+import com.xxl.job.admin.framework.util.I18nUtil;
+
+/**
+ * @author xuxueli 2020-10-29 21:11:23
+ */
+public enum ScheduleTypeEnum {
+
+ NONE(I18nUtil.getString("schedule_type_none"), new NoneScheduleType()),
+
+ /**
+ * schedule by cron
+ */
+ CRON(I18nUtil.getString("schedule_type_cron"), new CronScheduleType()),
+
+ /**
+ * schedule by fixed rate (in seconds)
+ */
+ FIX_RATE(I18nUtil.getString("schedule_type_fix_rate"), new FixRateScheduleType()),
+
+ /**
+ * schedule by fix delay (in seconds), after the last time
+ */
+ /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/;
+
+ private final String title;
+ private final ScheduleType scheduleType;;
+
+ ScheduleTypeEnum(String title, ScheduleType scheduleType) {
+ this.title = title;
+ this.scheduleType = scheduleType;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public ScheduleType getScheduleType() {
+ return scheduleType;
+ }
+
+ /**
+ * match by name
+ *
+ * @param name name of ScheduleTypeEnum
+ * @param defaultItem default item
+ * @return ScheduleTypeEnum
+ */
+ public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){
+ for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) {
+ if (item.name().equals(name)) {
+ return item;
+ }
+ }
+ return defaultItem;
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/CronScheduleType.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/CronScheduleType.java
new file mode 100644
index 00000000..1eca2fd4
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/CronScheduleType.java
@@ -0,0 +1,17 @@
+package com.xxl.job.admin.business.scheduler.type.strategy;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.scheduler.cron.CronExpression;
+import com.xxl.job.admin.business.scheduler.type.ScheduleType;
+
+import java.util.Date;
+
+public class CronScheduleType extends ScheduleType {
+
+ @Override
+ public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
+ // generate next trigger time, with cron
+ return new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime);
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/FixRateScheduleType.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/FixRateScheduleType.java
new file mode 100644
index 00000000..71805cfd
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/FixRateScheduleType.java
@@ -0,0 +1,26 @@
+package com.xxl.job.admin.business.scheduler.type.strategy;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.scheduler.type.ScheduleType;
+import com.xxl.tool.core.DateTool;
+
+import java.util.Date;
+
+public class FixRateScheduleType extends ScheduleType {
+
+ @Override
+ public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
+
+ // generate next trigger time, fix rate delay
+ Date nextTriggerTime = new Date(fromTime.getTime() + Long.parseLong(jobInfo.getScheduleConf()) * 1000L);
+
+ // assign second:
+ if (nextTriggerTime.getTime() % 1000 != 0) {
+ nextTriggerTime = DateTool.addSeconds(DateTool.setMilliseconds(nextTriggerTime, 0), 1);
+ }
+
+ return nextTriggerTime;
+
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/NoneScheduleType.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/NoneScheduleType.java
new file mode 100644
index 00000000..2a060abb
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/type/strategy/NoneScheduleType.java
@@ -0,0 +1,16 @@
+package com.xxl.job.admin.business.scheduler.type.strategy;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.job.admin.business.scheduler.type.ScheduleType;
+
+import java.util.Date;
+
+public class NoneScheduleType extends ScheduleType {
+
+ @Override
+ public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
+ // generate none trigger-time
+ return null;
+ }
+
+}
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/business/service/XxlJobService.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/service/XxlJobService.java
new file mode 100644
index 00000000..11b88f6d
--- /dev/null
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/business/service/XxlJobService.java
@@ -0,0 +1,63 @@
+package com.xxl.job.admin.business.service;
+
+import com.xxl.job.admin.business.model.XxlJobInfo;
+import com.xxl.sso.core.model.LoginInfo;
+import com.xxl.tool.response.PageModel;
+import com.xxl.tool.response.Response;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * core job action for xxl-job
+ *
+ * @author xuxueli 2016-5-28 15:30:33
+ */
+public interface XxlJobService {
+
+ /**
+ * page list
+ */
+ public Response> pageList(int offset, int pagesize, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author);
+
+ /**
+ * add job
+ */
+ public Response add(XxlJobInfo jobInfo, LoginInfo loginInfo);
+
+ /**
+ * update job
+ */
+ public Response update(XxlJobInfo jobInfo, LoginInfo loginInfo);
+
+ /**
+ * remove job
+ */
+ public Response remove(int id, LoginInfo loginInfo);
+
+ /**
+ * start job
+ */
+ public Response start(int id, LoginInfo loginInfo);
+
+ /**
+ * stop job
+ */
+ public Response stop(int id, LoginInfo loginInfo);
+
+ /**
+ * trigger
+ */
+ public Response trigger(LoginInfo loginInfo, int jobId, String executorParam, String addressList);
+
+ /**
+ * dashboard info
+ */
+ public Map dashboardInfo();
+
+ /**
+ * chart info
+ */
+ public Response