Объявление сложных выражений Cron все еще доставляет мне некоторые головные боли, особенно когда используются более сложные конструкции. В конце концов, можете ли вы сказать, когда сработает следующий триггер '0 0 17 L-3W 6-9 ? *'
'0 0 17 L-3W 6-9 ? *'
? Поскольку триггеры часто предназначены для запуска далеко в будущем, желательно предварительно их протестировать и убедиться, что они действительно сработают, когда мы думаем, что они сработают.
Кварцевый планировщик (я тестирую версию 2.1.6) не предоставляет прямой поддержки для этого, но легко создать некую простую функцию на основе существующих API, а именно CronExpression.getNextValidTimeAfter()
. Наша цель — определить метод, который будет возвращать следующие N
запланированных выполнений для данного выражения Cron. Мы не можем запросить все, так как некоторые триггеры (включая вышеупомянутый) не имеют даты окончания, повторяющейся бесконечно. Мы можем зависеть только от вышеупомянутого getNextValidTimeAfter()
который принимает дату в качестве аргумента и возвращает ближайшее время срабатывания T 1
после этой даты. Поэтому, если мы хотим найти второе запланированное выполнение, мы должны спросить о следующем выполнении после первого ( T 1
). И так далее. Давайте поместим это в код:
1
2
3
4
5
6
7
8
9
|
def findTriggerTimesIterative(expr : CronExpression, from : Date = new Date, max : Int = 100 ) : Seq[Date] = { val times = mutable.Buffer[Date]() var next = expr getNextValidTimeAfter from while (next ! = null && times.size < max) { times + = next next = expr getNextValidTimeAfter next } times } |
Если нет следующего времени срабатывания (например, триггер предполагается запустить только в 2012 году, и мы спрашиваем о времени срабатывания после 1 января 2013 года), возвращается null
. Немного краш-тестирования:
1
|
findTriggerTimesRecursive( new CronExpression( '0 0 17 L-3W 6-9 ? *' )) foreach println |
выходы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Thu Jun 27 17:00:00 CEST 2013 Mon Jul 29 17:00:00 CEST 2013 Wed Aug 28 17:00:00 CEST 2013 Fri Sep 27 17:00:00 CEST 2013 Fri Jun 27 17:00:00 CEST 2014 Mon Jul 28 17:00:00 CEST 2014 Thu Aug 28 17:00:00 CEST 2014 Fri Sep 26 17:00:00 CEST 2014 Fri Jun 26 17:00:00 CEST 2015 Tue Jul 28 17:00:00 CEST 2015 Fri Aug 28 17:00:00 CEST 2015 Mon Sep 28 17:00:00 CEST 2015 Mon Jun 27 17:00:00 CEST 2016 ... |
Надеюсь, что значение нашего сложного выражения Cron теперь стало более понятным: ближайший день недели ( W
) за три дня до конца месяца ( L-3
) в период с июня по сентябрь ( 6-9
) в 17:00:00 ( 0 0 17
) , Теперь я начал немного экспериментировать с различными реализациями, чтобы найти наиболее элегантную и подходящую для этой довольно простой задачи. Сначала я заметил, что проблема не итеративная, а рекурсивная: поиск следующих 100 времен выполнения эквивалентен нахождению первого выполнения и нахождению 99 оставшихся выполнений после первого:
1
2
3
4
5
6
7
8
9
|
def findTriggerTimesRecursive(expr : CronExpression, from : Date = new Date, max : Int = 100 ) : List[Date] = expr getNextValidTimeAfter from match { case null = > Nil case next = > if (max > 0 ) next :: findTriggerTimesRecursive(expr, next, max - 1 ) else Nil } |
Похоже, что реализация намного проще: нет совпадений — вернуть пустой список ( Nil
). Матч найден — верните его в начале следующего матча, если мы не собрали достаточно дат. Есть одна проблема с этой реализацией, хотя она не хвостовая рекурсия . Очень часто это можно изменить введением второй функции и накоплением промежуточных результатов в аргументах:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
def findTriggerTimesTailRecursive(expr : CronExpression, from : Date = new Date, max : Int = 100 ) = { @ tailrec def accum(curFrom : Date, curMax : Int, acc : List[Date]) : List[Date] = { expr getNextValidTimeAfter curFrom match { case null = > acc case next = > if (curMax > 0 ) accum(next, curMax - 1 , next :: acc) else acc } } accum(from, max, Nil) } |
Немного сложнее, но, по крайней мере, StackOverflowError
не разбудит нас среди ночи. Кстати, я только что заметил, что IntelliJ IDEA не только показывает значки, идентифицирующие рекурсию (см. Рядом с номером строки), но также использует различные значки, когда используется оптимизация хвостового вызова (!):
Поэтому я подумал, что это лучшее, что я могу получить, когда пришла другая идея. Прежде всего, искусственный max
предел (по умолчанию равный 100) казался неловким. Кроме того, зачем накапливать все результаты, если мы можем вычислять их на лету, один за другим? Это когда я понял, что мне не нужны Seq
или List
, мне нужен Iterator[Date]
!
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class TimeIterator(expr : CronExpression, from : Date = new Date) extends Iterator[Date] { private var cur = expr getNextValidTimeAfter from def hasNext = cur ! = null def next() = if (hasNext) { val toReturn = cur cur = expr getNextValidTimeAfter cur toReturn } else { throw new NoSuchElementException } } |
Я потратил некоторые усилия, чтобы уменьшить истинную ветвь в одну toReturn
и избежать промежуточной переменной toReturn
. Это возможно, но для ясности (и пощадить ваши глаза) я не буду раскрывать это * . Но почему итератор, как известно, менее гибкий и приятный в использовании? Ну, во-первых, это позволяет нам лениво генерировать время следующего запуска, поэтому мы не платим за то, что не используем. Также промежуточные результаты нигде не хранятся, поэтому мы можем сэкономить память. И поскольку все, что работает для последовательностей, работает и для итераторов, мы можем легко работать с итераторами в Scala, например, печатать ( брать ) первые 10 дат:
1
|
new TimeIterator(expr) take 10 foreach println |
Заманчиво сделать небольшой тест, сравнивающий различные реализации (здесь, используя суппорт ):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
object FindTriggerTimesBenchmark extends App { Runner.main(classOf[FindTriggerTimesBenchmark], Array( '--trials' , '1' )) } class FindTriggerTimesBenchmark extends SimpleBenchmark { val expr = new CronExpression( '0 0 17 L-3W 6-9 ? *' ) def timeIterative(reps : Int) { for (i <- 1 to reps) { findTriggerTimesIterative(expr) } } def timeRecursive(reps : Int) { for (i <- 1 to reps) { findTriggerTimesRecursive(expr) } } def timeTailRecursive(reps : Int) { for (i <- 1 to reps) { findTriggerTimesTailRecursive(expr) } } def timeUsedIterator(reps : Int) { for (i <- 1 to reps) { ( new TimeIterator(expr) take 100 ).toList } } def timeNotUsedIterator(reps : Int) { for (i <- 1 to reps) { new TimeIterator(expr) } } } |
Похоже, что изменения реализации оказывают незначительное влияние на время, так как большая часть ЦП, вероятно, сгорела внутри
getNextValidTimeAfter()
.
Что мы узнали сегодня?
- не думайте слишком много о производительности, если у вас действительно нет проблем. Стремитесь к лучшему дизайну и простейшей реализации. Подумайте много о структурах данных, которые вы хотите использовать для представления вашей проблемы и решения. В этой (тривиальной на первый взгляд) проблеме
Iterator
(лениво оцениваемый, возможно, бесконечный поток элементов) оказался лучшим подходом
* ОК, вот как. Подсказка: назначение имеет тип Unit
и кортеж (Date, Unit)
здесь:
1
2
3
4
|
def next() = if (hasNext) (cur, cur = expr getNextValidTimeAfter cur). _ 1 else throw new NoSuchElementException |
Ссылка: Тестирование выражений Quartz Cron от нашего партнера по JCG Томаша Нуркевича в блоге Java и соседях .