Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ object MaxSecondsBetweenSuccessesFormatter extends Formatter[Int] {
data,
)
} else {
parsing(_.toLong, "try again.", Nil)("simple.repeatInterval", data)
parsing(expr => Some(expr.toInt), "try again.", Nil)("simple.repeatInterval", data)
}
}
_ <- Either.cond(
maxSecondsBetweenSuccesses > maxIntervalTime,
maxIntervalTime.forall(_ < maxSecondsBetweenSuccesses),
maxSecondsBetweenSuccesses,
List(
FormError(
Expand Down
23 changes: 18 additions & 5 deletions admin/app/com/lucidchart/piezo/admin/utils/CronHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,41 @@ object CronHelper extends Logging {
* problem to solve perfectly, so this represents a "best effort approach" - the goal is to handle the most
* expressions with the least amount of complexity.
*
* Returns an option of the cron's max interval. If the cron only runs in the past, or is too far in the future, the
* method returns None
*
* Known limitations:
* 1. Daylight savings
* 1. Complex year subexpressions
* 1. Quartz scheduler doesn't allow cron expressions that execute only in the past, or those that execute for the
* first time more than 100 years from when they are scheduled
* @param cronExpression
*/
def getMaxInterval(cronExpression: String): Long = {
def getMaxInterval(cronExpression: String): Option[Int] = {
try {
val (secondsMinutesHourStrings, dayStrings) = cronExpression.split("\\s+").splitAt(3)
val subexpressions = getSubexpressions(secondsMinutesHourStrings :+ dayStrings.mkString(" ")).reverse

// find the largest subexpression that is not continuously triggering (*)
val outermostIndex = subexpressions.indexWhere(!_.isContinuouslyTriggering)
if (outermostIndex == NON_EXISTENT) 1
if (outermostIndex == NON_EXISTENT) Some(1)
else {
// get the max interval for this expression
val outermost = subexpressions(outermostIndex)
if (outermost.maxInterval == IMPOSSIBLE_MAX_INTERVAL) IMPOSSIBLE_MAX_INTERVAL
// When cron represents a past-time (or time too far in the future), the Long.MaxValue needs to be passed in
if (outermost.maxInterval == IMPOSSIBLE_MAX_INTERVAL) None
else {
// subtract the inner intervals of the smaller, nested subexpressions
val nested = subexpressions.slice(outermostIndex + 1, subexpressions.size)
val innerIntervalsOfNested = nested.collect { case expr: BoundSubexpression => expr.innerInterval }.sum
outermost.maxInterval - innerIntervalsOfNested
longToIntBounded(outermost.maxInterval - innerIntervalsOfNested)
}
}

} catch {
case NonFatal(e) =>
logger.error("Failed to validate cron expression", e)
DEFAULT_MAX_INTERVAL
Some(DEFAULT_MAX_INTERVAL)
}
}

Expand All @@ -56,6 +62,13 @@ object CronHelper extends Logging {
.map { case (str, cronType) => cronType(str) }
.toIndexedSeq
}

// Need to bound Long values to Int when validating, since the formatter expects Int type
private def longToIntBounded(l: Long): Option[Int] = {
if (l >= Int.MaxValue) None
else if (l < Int.MinValue) None
else Some(l.toInt)
}
}

case class Seconds(str: String) extends BoundSubexpression(str, x => s"$x * * ? * *", ChronoUnit.SECONDS, 60)
Expand Down
68 changes: 35 additions & 33 deletions admin/test/com/lucidchart/piezo/admin/util/CronHelperTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,51 @@ class CronHelperTest extends Specification {
val WEEK: Int = 7 * DAY
val YEAR: Int = 365 * DAY
val LEAP_YEAR: Int = YEAR + DAY
val IMPOSSIBLE: Long = Long.MaxValue

def maxInterval(str: String): Long = CronHelper.getMaxInterval(str)
def maxInterval(str: String): Option[Int] = CronHelper.getMaxInterval(str)

"CronHelper" should {
"validate basic cron expressions" in {
maxInterval("* * * * * ?") mustEqual SECOND // every second
maxInterval("0 * * * * ?") mustEqual MINUTE // second 0 of every minute
maxInterval("0 0 * * * ?") mustEqual HOUR // second 0 during minute 0 of every hour
maxInterval("0 0 0 * * ?") mustEqual DAY // second 0 during minute 0 during hour 0 of every day
maxInterval("* 0 * * * ?") mustEqual (HOUR - MINUTE + SECOND) // every second during minute 0
maxInterval("* * 0 * * ?") mustEqual (DAY - HOUR + SECOND)
maxInterval("* * * * * ?") must beSome(SECOND) // every second
maxInterval("0 * * * * ?") must beSome(MINUTE) // second 0 of every minute
maxInterval("0 0 * * * ?") must beSome(HOUR) // second 0 during minute 0 of every hour
maxInterval("0 0 0 * * ?") must beSome(DAY) // second 0 during minute 0 during hour 0 of every day
maxInterval("* 0 * * * ?") must beSome(HOUR - MINUTE + SECOND) // every second during minute 0
maxInterval("* * 0 * * ?") must beSome(DAY - HOUR + SECOND)
}

"validate more basic cron expressions" in {
maxInterval("0/1 0-59 */1 * * ?") mustEqual SECOND // variations on 1 second
maxInterval("* * 0-23 * * ?") mustEqual SECOND
maxInterval("22 2/6 * * * ?") mustEqual 6 * MINUTE // 22nd second of every 6th minute after minute 2
maxInterval("*/15 * * * * ?") mustEqual 15 * SECOND
maxInterval("30 10 */1 * * ?") mustEqual HOUR
maxInterval("15 * * * * ?") mustEqual MINUTE
maxInterval("3,2,1,0 45,44,16,15 6,5,4 * * ? *") mustEqual (21 * HOUR + 29 * MINUTE + 57 * SECOND)
maxInterval("50-0 30-40 14-12 * * ?") mustEqual (1 * HOUR + 49 * MINUTE + 1 * SECOND)
maxInterval("0 0 8-4 * * ?") mustEqual 4 * HOUR
maxInterval("0 0 0/6 * * ? *") mustEqual 6 * HOUR
maxInterval("0 10,20,30 * * ? *") mustEqual 40 * MINUTE
maxInterval("0-10/2 0-5,20-25 0,5-11/2,20-23 * ? *") mustEqual 8 * HOUR + 34 * MINUTE + 50 * SECOND
maxInterval("0/1 0-59 */1 * * ?") must beSome(SECOND) // variations on 1 second
maxInterval("* * 0-23 * * ?") must beSome(SECOND)
maxInterval("22 2/6 * * * ?") must beSome(6 * MINUTE) // 22nd second of every 6th minute after minute 2
maxInterval("*/15 * * * * ?") must beSome(15 * SECOND)
maxInterval("30 10 */1 * * ?") must beSome(HOUR)
maxInterval("15 * * * * ?") must beSome(MINUTE)
maxInterval("3,2,1,0 45,44,16,15 6,5,4 * * ? *") must beSome(21 * HOUR + 29 * MINUTE + 57 * SECOND)
maxInterval("50-0 30-40 14-12 * * ?") must beSome(1 * HOUR + 49 * MINUTE + 1 * SECOND)
maxInterval("0 0 8-4 * * ?") must beSome(4 * HOUR)
maxInterval("0 0 0/6 * * ? *") must beSome(6 * HOUR)
maxInterval("0 10,20,30 * * ? *") must beSome(40 * MINUTE)
maxInterval("0-10/2 0-5,20-25 0,5-11/2,20-23 * ? *") must beSome(8 * HOUR + 34 * MINUTE + 50 * SECOND)
}

"validate complex cron expressions" in {
maxInterval("0/15 * * 1-12 * ?") mustEqual 19 * DAY + 15 * SECOND // every 15 seconds on days 1-12 of the month
maxInterval("* * * * 1-11 ?") mustEqual 31 * DAY + SECOND // every second of every month except for december
maxInterval("* * * * * ? 1998") mustEqual IMPOSSIBLE // every second of 1998
maxInterval("0 0 0 29 2 ? *") mustEqual 8 * YEAR + DAY // 8 years since we skip leap day roughly every 100 years
maxInterval("* * * 29 2 ? *") mustEqual 8 * YEAR + SECOND // every second on leap day
maxInterval("0 11 11 11 11 ?") mustEqual LEAP_YEAR // every november 11th at 11:11am
maxInterval("1 2 3 ? * 6") mustEqual WEEK // every saturday
maxInterval("0 15 10 ? * 6#3") mustEqual 5 * WEEK // third saturday of every month
maxInterval("0 15 10 ? * MON-FRI") mustEqual 3 * DAY // every weekday
maxInterval("0 0 0/6 * 1,2,3,4,5,6,7,8,9,10,11,12 ? *") mustEqual DAY - (18 * HOUR)
maxInterval("* * * 1-31 * ?") mustEqual SECOND
maxInterval("* * * * 1-12 ?") mustEqual SECOND
maxInterval("* * * ? * 1-7") mustEqual SECOND
maxInterval("0/15 * * 1-12 * ?") must beSome(19 * DAY + 15 * SECOND) // every 15 seconds on days 1-12 of the month
maxInterval("* * * * 1-11 ?") must beSome(31 * DAY + SECOND) // every second of every month except for december
maxInterval("* * * * * ? 1998") must beNone // every second of 1998
maxInterval("0 0 20 ? 10 WED#2 2015") must beNone // a single moment in the past
maxInterval("0 0 0 29 2 ? *") must beSome(
8 * YEAR + DAY,
) // 8 years since we skip leap day roughly every 100 years
maxInterval("* * * 29 2 ? *") must beSome(8 * YEAR + SECOND) // every second on leap day
maxInterval("0 11 11 11 11 ?") must beSome(LEAP_YEAR) // every november 11th at 11:11am
maxInterval("1 2 3 ? * 6") must beSome(WEEK) // every saturday
maxInterval("0 15 10 ? * 6#3") must beSome(5 * WEEK) // third saturday of every month
maxInterval("0 15 10 ? * MON-FRI") must beSome(3 * DAY) // every weekday
maxInterval("0 0 0/6 * 1,2,3,4,5,6,7,8,9,10,11,12 ? *") must beSome(DAY - (18 * HOUR))
maxInterval("* * * 1-31 * ?") must beSome(SECOND)
maxInterval("* * * * 1-12 ?") must beSome(SECOND)
maxInterval("* * * ? * 1-7") must beSome(SECOND)
}
}
}