You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by ke...@apache.org on 2014/05/15 04:34:41 UTC

[1/5] CronScheduler based on Quartz

Repository: incubator-aurora
Updated Branches:
  refs/heads/master 2759696ca -> 3361dbedf


http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/resources/org/apache/aurora/scheduler/cron/expected-predictions.json
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/aurora/scheduler/cron/expected-predictions.json b/src/test/resources/org/apache/aurora/scheduler/cron/expected-predictions.json
new file mode 100644
index 0000000..dced8b4
--- /dev/null
+++ b/src/test/resources/org/apache/aurora/scheduler/cron/expected-predictions.json
@@ -0,0 +1,3332 @@
+[
+    {
+        "schedule": "0 20 * * 1",
+        "triggerTimes": [
+            417600000,
+            1022400000,
+            1627200000,
+            2232000000,
+            2836800000,
+            3441600000,
+            4046400000,
+            4651200000,
+            5256000000,
+            5860800000
+        ]
+    },
+    {
+        "schedule": "11    *   *   *   *",
+        "triggerTimes": [
+            660000,
+            4260000,
+            7860000,
+            11460000,
+            15060000,
+            18660000,
+            22260000,
+            25860000,
+            29460000,
+            33060000
+        ]
+    },
+    {
+        "schedule": "04 02 * * *",
+        "triggerTimes": [
+            7440000,
+            93840000,
+            180240000,
+            266640000,
+            353040000,
+            439440000,
+            525840000,
+            612240000,
+            698640000,
+            785040000
+        ]
+    },
+    {
+        "schedule": "09 22 * * *",
+        "triggerTimes": [
+            79740000,
+            166140000,
+            252540000,
+            338940000,
+            425340000,
+            511740000,
+            598140000,
+            684540000,
+            770940000,
+            857340000
+        ]
+    },
+    {
+        "schedule": "1-56/5 * * * *",
+        "triggerTimes": [
+            60000,
+            360000,
+            660000,
+            960000,
+            1260000,
+            1560000,
+            1860000,
+            2160000,
+            2460000,
+            2760000
+        ]
+    },
+    {
+        "schedule": "05 02,08,12 * * *",
+        "triggerTimes": [
+            7500000,
+            29100000,
+            43500000,
+            93900000,
+            115500000,
+            129900000,
+            180300000,
+            201900000,
+            216300000,
+            266700000
+        ]
+    },
+    {
+        "schedule": "26 * * * *",
+        "triggerTimes": [
+            1560000,
+            5160000,
+            8760000,
+            12360000,
+            15960000,
+            19560000,
+            23160000,
+            26760000,
+            30360000,
+            33960000
+        ]
+    },
+    {
+        "schedule": "3,43 * * * *",
+        "triggerTimes": [
+            180000,
+            2580000,
+            3780000,
+            6180000,
+            7380000,
+            9780000,
+            10980000,
+            13380000,
+            14580000,
+            16980000
+        ]
+    },
+    {
+        "schedule": "0 17-23 * * 1-5",
+        "triggerTimes": [
+            61200000,
+            64800000,
+            68400000,
+            72000000,
+            75600000,
+            79200000,
+            82800000,
+            147600000,
+            151200000,
+            154800000
+        ]
+    },
+    {
+        "schedule": "0 0,12 * * *",
+        "triggerTimes": [
+            43200000,
+            86400000,
+            129600000,
+            172800000,
+            216000000,
+            259200000,
+            302400000,
+            345600000,
+            388800000,
+            432000000
+        ]
+    },
+    {
+        "schedule": "10 02,08,12 * * *",
+        "triggerTimes": [
+            7800000,
+            29400000,
+            43800000,
+            94200000,
+            115800000,
+            130200000,
+            180600000,
+            202200000,
+            216600000,
+            267000000
+        ]
+    },
+    {
+        "schedule": "50 */4 * * *",
+        "triggerTimes": [
+            3000000,
+            17400000,
+            31800000,
+            46200000,
+            60600000,
+            75000000,
+            89400000,
+            103800000,
+            118200000,
+            132600000
+        ]
+    },
+    {
+        "schedule": "10 02,08,14,20 * * *",
+        "triggerTimes": [
+            7800000,
+            29400000,
+            51000000,
+            72600000,
+            94200000,
+            115800000,
+            137400000,
+            159000000,
+            180600000,
+            202200000
+        ]
+    },
+    {
+        "schedule": "0 */6 * * *",
+        "triggerTimes": [
+            21600000,
+            43200000,
+            64800000,
+            86400000,
+            108000000,
+            129600000,
+            151200000,
+            172800000,
+            194400000,
+            216000000
+        ]
+    },
+    {
+        "schedule": "* * * * *",
+        "triggerTimes": [
+            60000,
+            120000,
+            180000,
+            240000,
+            300000,
+            360000,
+            420000,
+            480000,
+            540000,
+            600000
+        ]
+    },
+    {
+        "schedule": "30 15 * * *,",
+        "triggerTimes": [
+            55800000,
+            142200000,
+            228600000,
+            315000000,
+            401400000,
+            487800000,
+            574200000,
+            660600000,
+            747000000,
+            833400000
+        ]
+    },
+    {
+        "schedule": "00 11 * * *",
+        "triggerTimes": [
+            39600000,
+            126000000,
+            212400000,
+            298800000,
+            385200000,
+            471600000,
+            558000000,
+            644400000,
+            730800000,
+            817200000
+        ]
+    },
+    {
+        "schedule": "55 06 * * *",
+        "triggerTimes": [
+            24900000,
+            111300000,
+            197700000,
+            284100000,
+            370500000,
+            456900000,
+            543300000,
+            629700000,
+            716100000,
+            802500000
+        ]
+    },
+    {
+        "schedule": "0 4 * * *",
+        "triggerTimes": [
+            14400000,
+            100800000,
+            187200000,
+            273600000,
+            360000000,
+            446400000,
+            532800000,
+            619200000,
+            705600000,
+            792000000
+        ]
+    },
+    {
+        "schedule": "55 */1 * * *",
+        "triggerTimes": [
+            3300000,
+            6900000,
+            10500000,
+            14100000,
+            17700000,
+            21300000,
+            24900000,
+            28500000,
+            32100000,
+            35700000
+        ]
+    },
+    {
+        "schedule": "15 */3 * * *",
+        "triggerTimes": [
+            900000,
+            11700000,
+            22500000,
+            33300000,
+            44100000,
+            54900000,
+            65700000,
+            76500000,
+            87300000,
+            98100000
+        ]
+    },
+    {
+        "schedule": "42 8,12,16 * * *",
+        "triggerTimes": [
+            31320000,
+            45720000,
+            60120000,
+            117720000,
+            132120000,
+            146520000,
+            204120000,
+            218520000,
+            232920000,
+            290520000
+        ]
+    },
+    {
+        "schedule": "23 * * * *",
+        "triggerTimes": [
+            1380000,
+            4980000,
+            8580000,
+            12180000,
+            15780000,
+            19380000,
+            22980000,
+            26580000,
+            30180000,
+            33780000
+        ]
+    },
+    {
+        "schedule": "10 16 * * *",
+        "triggerTimes": [
+            58200000,
+            144600000,
+            231000000,
+            317400000,
+            403800000,
+            490200000,
+            576600000,
+            663000000,
+            749400000,
+            835800000
+        ]
+    },
+    {
+        "schedule": "*/30 * * * *",
+        "triggerTimes": [
+            1800000,
+            3600000,
+            5400000,
+            7200000,
+            9000000,
+            10800000,
+            12600000,
+            14400000,
+            16200000,
+            18000000
+        ]
+    },
+    {
+        "schedule": "20 */3 * * *",
+        "triggerTimes": [
+            1200000,
+            12000000,
+            22800000,
+            33600000,
+            44400000,
+            55200000,
+            66000000,
+            76800000,
+            87600000,
+            98400000
+        ]
+    },
+    {
+        "schedule": "8 6,12,18 * * *",
+        "triggerTimes": [
+            22080000,
+            43680000,
+            65280000,
+            108480000,
+            130080000,
+            151680000,
+            194880000,
+            216480000,
+            238080000,
+            281280000
+        ]
+    },
+    {
+        "schedule": "30 7,12,22 * * *",
+        "triggerTimes": [
+            27000000,
+            45000000,
+            81000000,
+            113400000,
+            131400000,
+            167400000,
+            199800000,
+            217800000,
+            253800000,
+            286200000
+        ]
+    },
+    {
+        "schedule": "0 0 12 * *",
+        "triggerTimes": [
+            950400000,
+            3628800000,
+            6048000000,
+            8726400000,
+            11318400000,
+            13996800000,
+            16588800000,
+            19267200000,
+            21945600000,
+            24537600000
+        ]
+    },
+    {
+        "schedule": "17 5,8,13,16,19 * * *",
+        "triggerTimes": [
+            19020000,
+            29820000,
+            47820000,
+            58620000,
+            69420000,
+            105420000,
+            116220000,
+            134220000,
+            145020000,
+            155820000
+        ]
+    },
+    {
+        "schedule": "27 8,20 * * *",
+        "triggerTimes": [
+            30420000,
+            73620000,
+            116820000,
+            160020000,
+            203220000,
+            246420000,
+            289620000,
+            332820000,
+            376020000,
+            419220000
+        ]
+    },
+    {
+        "schedule": "15 */6 * * *",
+        "triggerTimes": [
+            900000,
+            22500000,
+            44100000,
+            65700000,
+            87300000,
+            108900000,
+            130500000,
+            152100000,
+            173700000,
+            195300000
+        ]
+    },
+    {
+        "schedule": "01 15 * * *",
+        "triggerTimes": [
+            54060000,
+            140460000,
+            226860000,
+            313260000,
+            399660000,
+            486060000,
+            572460000,
+            658860000,
+            745260000,
+            831660000
+        ]
+    },
+    {
+        "schedule": "0 18 * * *",
+        "triggerTimes": [
+            64800000,
+            151200000,
+            237600000,
+            324000000,
+            410400000,
+            496800000,
+            583200000,
+            669600000,
+            756000000,
+            842400000
+        ]
+    },
+    {
+        "schedule": "24 * * * *",
+        "triggerTimes": [
+            1440000,
+            5040000,
+            8640000,
+            12240000,
+            15840000,
+            19440000,
+            23040000,
+            26640000,
+            30240000,
+            33840000
+        ]
+    },
+    {
+        "schedule": "18 00 * * *",
+        "triggerTimes": [
+            1080000,
+            87480000,
+            173880000,
+            260280000,
+            346680000,
+            433080000,
+            519480000,
+            605880000,
+            692280000,
+            778680000
+        ]
+    },
+    {
+        "schedule": "0 16 * * *",
+        "triggerTimes": [
+            57600000,
+            144000000,
+            230400000,
+            316800000,
+            403200000,
+            489600000,
+            576000000,
+            662400000,
+            748800000,
+            835200000
+        ]
+    },
+    {
+        "schedule": "45 5 * * *",
+        "triggerTimes": [
+            20700000,
+            107100000,
+            193500000,
+            279900000,
+            366300000,
+            452700000,
+            539100000,
+            625500000,
+            711900000,
+            798300000
+        ]
+    },
+    {
+        "schedule": "0 18 * * 4",
+        "triggerTimes": [
+            64800000,
+            669600000,
+            1274400000,
+            1879200000,
+            2484000000,
+            3088800000,
+            3693600000,
+            4298400000,
+            4903200000,
+            5508000000
+        ]
+    },
+    {
+        "schedule": "30 19 * * *",
+        "triggerTimes": [
+            70200000,
+            156600000,
+            243000000,
+            329400000,
+            415800000,
+            502200000,
+            588600000,
+            675000000,
+            761400000,
+            847800000
+        ]
+    },
+    {
+        "schedule": "0 13 * * 2",
+        "triggerTimes": [
+            478800000,
+            1083600000,
+            1688400000,
+            2293200000,
+            2898000000,
+            3502800000,
+            4107600000,
+            4712400000,
+            5317200000,
+            5922000000
+        ]
+    },
+    {
+        "schedule": "25 17,20,21,23 * * *",
+        "triggerTimes": [
+            62700000,
+            73500000,
+            77100000,
+            84300000,
+            149100000,
+            159900000,
+            163500000,
+            170700000,
+            235500000,
+            246300000
+        ]
+    },
+    {
+        "schedule": "0 13 * * 3",
+        "triggerTimes": [
+            565200000,
+            1170000000,
+            1774800000,
+            2379600000,
+            2984400000,
+            3589200000,
+            4194000000,
+            4798800000,
+            5403600000,
+            6008400000
+        ]
+    },
+    {
+        "schedule": "58 */2 * * *",
+        "triggerTimes": [
+            3480000,
+            10680000,
+            17880000,
+            25080000,
+            32280000,
+            39480000,
+            46680000,
+            53880000,
+            61080000,
+            68280000
+        ]
+    },
+    {
+        "schedule": "0 9 4,18 * *",
+        "triggerTimes": [
+            291600000,
+            1501200000,
+            2970000000,
+            4179600000,
+            5389200000,
+            6598800000,
+            8067600000,
+            9277200000,
+            10659600000,
+            11869200000
+        ]
+    },
+    {
+        "schedule": "37    */6 *   *   *",
+        "triggerTimes": [
+            2220000,
+            23820000,
+            45420000,
+            67020000,
+            88620000,
+            110220000,
+            131820000,
+            153420000,
+            175020000,
+            196620000
+        ]
+    },
+    {
+        "schedule": "00 14 * * *",
+        "triggerTimes": [
+            50400000,
+            136800000,
+            223200000,
+            309600000,
+            396000000,
+            482400000,
+            568800000,
+            655200000,
+            741600000,
+            828000000
+        ]
+    },
+    {
+        "schedule": "0 * * * *",
+        "triggerTimes": [
+            3600000,
+            7200000,
+            10800000,
+            14400000,
+            18000000,
+            21600000,
+            25200000,
+            28800000,
+            32400000,
+            36000000
+        ]
+    },
+    {
+        "schedule": "29 9,16,22 * * *",
+        "triggerTimes": [
+            34140000,
+            59340000,
+            80940000,
+            120540000,
+            145740000,
+            167340000,
+            206940000,
+            232140000,
+            253740000,
+            293340000
+        ]
+    },
+    {
+        "schedule": "37 3 * * *",
+        "triggerTimes": [
+            13020000,
+            99420000,
+            185820000,
+            272220000,
+            358620000,
+            445020000,
+            531420000,
+            617820000,
+            704220000,
+            790620000
+        ]
+    },
+    {
+        "schedule": "*/5 * * * *",
+        "triggerTimes": [
+            300000,
+            600000,
+            900000,
+            1200000,
+            1500000,
+            1800000,
+            2100000,
+            2400000,
+            2700000,
+            3000000
+        ]
+    },
+    {
+        "schedule": "7 */2 * * *",
+        "triggerTimes": [
+            420000,
+            7620000,
+            14820000,
+            22020000,
+            29220000,
+            36420000,
+            43620000,
+            50820000,
+            58020000,
+            65220000
+        ]
+    },
+    {
+        "schedule": "55 07 * * *",
+        "triggerTimes": [
+            28500000,
+            114900000,
+            201300000,
+            287700000,
+            374100000,
+            460500000,
+            546900000,
+            633300000,
+            719700000,
+            806100000
+        ]
+    },
+    {
+        "schedule": "0 19 * * *",
+        "triggerTimes": [
+            68400000,
+            154800000,
+            241200000,
+            327600000,
+            414000000,
+            500400000,
+            586800000,
+            673200000,
+            759600000,
+            846000000
+        ]
+    },
+    {
+        "schedule": "15 */2 * * *",
+        "triggerTimes": [
+            900000,
+            8100000,
+            15300000,
+            22500000,
+            29700000,
+            36900000,
+            44100000,
+            51300000,
+            58500000,
+            65700000
+        ]
+    },
+    {
+        "schedule": "17 00 * * *",
+        "triggerTimes": [
+            1020000,
+            87420000,
+            173820000,
+            260220000,
+            346620000,
+            433020000,
+            519420000,
+            605820000,
+            692220000,
+            778620000
+        ]
+    },
+    {
+        "schedule": "0 0 * * 1",
+        "triggerTimes": [
+            345600000,
+            950400000,
+            1555200000,
+            2160000000,
+            2764800000,
+            3369600000,
+            3974400000,
+            4579200000,
+            5184000000,
+            5788800000
+        ]
+    },
+    {
+        "schedule": "29 */4 * * *",
+        "triggerTimes": [
+            1740000,
+            16140000,
+            30540000,
+            44940000,
+            59340000,
+            73740000,
+            88140000,
+            102540000,
+            116940000,
+            131340000
+        ]
+    },
+    {
+        "schedule": "0 23 * * *",
+        "triggerTimes": [
+            82800000,
+            169200000,
+            255600000,
+            342000000,
+            428400000,
+            514800000,
+            601200000,
+            687600000,
+            774000000,
+            860400000
+        ]
+    },
+    {
+        "schedule": "0 7 * * *",
+        "triggerTimes": [
+            25200000,
+            111600000,
+            198000000,
+            284400000,
+            370800000,
+            457200000,
+            543600000,
+            630000000,
+            716400000,
+            802800000
+        ]
+    },
+    {
+        "schedule": "12 * * * *",
+        "triggerTimes": [
+            720000,
+            4320000,
+            7920000,
+            11520000,
+            15120000,
+            18720000,
+            22320000,
+            25920000,
+            29520000,
+            33120000
+        ]
+    },
+    {
+        "schedule": "0 23 * * 3",
+        "triggerTimes": [
+            601200000,
+            1206000000,
+            1810800000,
+            2415600000,
+            3020400000,
+            3625200000,
+            4230000000,
+            4834800000,
+            5439600000,
+            6044400000
+        ]
+    },
+    {
+        "schedule": "23 */4 * * *",
+        "triggerTimes": [
+            1380000,
+            15780000,
+            30180000,
+            44580000,
+            58980000,
+            73380000,
+            87780000,
+            102180000,
+            116580000,
+            130980000
+        ]
+    },
+    {
+        "schedule": "30 1-23/2 * * *",
+        "triggerTimes": [
+            5400000,
+            12600000,
+            19800000,
+            27000000,
+            34200000,
+            41400000,
+            48600000,
+            55800000,
+            63000000,
+            70200000
+        ]
+    },
+    {
+        "schedule": "5,15,25,35,45,55 * * * *",
+        "triggerTimes": [
+            300000,
+            900000,
+            1500000,
+            2100000,
+            2700000,
+            3300000,
+            3900000,
+            4500000,
+            5100000,
+            5700000
+        ]
+    },
+    {
+        "schedule": "23 1,11,21 * * *",
+        "triggerTimes": [
+            4980000,
+            40980000,
+            76980000,
+            91380000,
+            127380000,
+            163380000,
+            177780000,
+            213780000,
+            249780000,
+            264180000
+        ]
+    },
+    {
+        "schedule": "15 04,10,16,22 * * *",
+        "triggerTimes": [
+            15300000,
+            36900000,
+            58500000,
+            80100000,
+            101700000,
+            123300000,
+            144900000,
+            166500000,
+            188100000,
+            209700000
+        ]
+    },
+    {
+        "schedule": "*/20  *   *   *   *",
+        "triggerTimes": [
+            1200000,
+            2400000,
+            3600000,
+            4800000,
+            6000000,
+            7200000,
+            8400000,
+            9600000,
+            10800000,
+            12000000
+        ]
+    },
+    {
+        "schedule": "12,42 * * * *",
+        "triggerTimes": [
+            720000,
+            2520000,
+            4320000,
+            6120000,
+            7920000,
+            9720000,
+            11520000,
+            13320000,
+            15120000,
+            16920000
+        ]
+    },
+    {
+        "schedule": "26 2,6,10,14,18,22 * * *",
+        "triggerTimes": [
+            8760000,
+            23160000,
+            37560000,
+            51960000,
+            66360000,
+            80760000,
+            95160000,
+            109560000,
+            123960000,
+            138360000
+        ]
+    },
+    {
+        "schedule": "0 3,6,9,12,15,18,21 * * *",
+        "triggerTimes": [
+            10800000,
+            21600000,
+            32400000,
+            43200000,
+            54000000,
+            64800000,
+            75600000,
+            97200000,
+            108000000,
+            118800000
+        ]
+    },
+    {
+        "schedule": "25 14 * * *",
+        "triggerTimes": [
+            51900000,
+            138300000,
+            224700000,
+            311100000,
+            397500000,
+            483900000,
+            570300000,
+            656700000,
+            743100000,
+            829500000
+        ]
+    },
+    {
+        "schedule": "0 5 * * *,",
+        "triggerTimes": [
+            18000000,
+            104400000,
+            190800000,
+            277200000,
+            363600000,
+            450000000,
+            536400000,
+            622800000,
+            709200000,
+            795600000
+        ]
+    },
+    {
+        "schedule": "43 * * * *",
+        "triggerTimes": [
+            2580000,
+            6180000,
+            9780000,
+            13380000,
+            16980000,
+            20580000,
+            24180000,
+            27780000,
+            31380000,
+            34980000
+        ]
+    },
+    {
+        "schedule": "39 6,12,16 * * *",
+        "triggerTimes": [
+            23940000,
+            45540000,
+            59940000,
+            110340000,
+            131940000,
+            146340000,
+            196740000,
+            218340000,
+            232740000,
+            283140000
+        ]
+    },
+    {
+        "schedule": "0 9 1 * *",
+        "triggerTimes": [
+            32400000,
+            2710800000,
+            5130000000,
+            7808400000,
+            10400400000,
+            13078800000,
+            15670800000,
+            18349200000,
+            21027600000,
+            23619600000
+        ]
+    },
+    {
+        "schedule": "14-59/30 * * * *",
+        "triggerTimes": [
+            840000,
+            2640000,
+            4440000,
+            6240000,
+            8040000,
+            9840000,
+            11640000,
+            13440000,
+            15240000,
+            17040000
+        ]
+    },
+    {
+        "schedule": "0 0 * * *",
+        "triggerTimes": [
+            86400000,
+            172800000,
+            259200000,
+            345600000,
+            432000000,
+            518400000,
+            604800000,
+            691200000,
+            777600000,
+            864000000
+        ]
+    },
+    {
+        "schedule": "0 */3 * * *",
+        "triggerTimes": [
+            10800000,
+            21600000,
+            32400000,
+            43200000,
+            54000000,
+            64800000,
+            75600000,
+            86400000,
+            97200000,
+            108000000
+        ]
+    },
+    {
+        "schedule": "16 5,13,21 * * *",
+        "triggerTimes": [
+            18960000,
+            47760000,
+            76560000,
+            105360000,
+            134160000,
+            162960000,
+            191760000,
+            220560000,
+            249360000,
+            278160000
+        ]
+    },
+    {
+        "schedule": "30 18,23 * * MON-FRI",
+        "triggerTimes": [
+            66600000,
+            84600000,
+            153000000,
+            171000000,
+            412200000,
+            430200000,
+            498600000,
+            516600000,
+            585000000,
+            603000000
+        ]
+    },
+    {
+        "schedule": "0,15,30,45 * * * *",
+        "triggerTimes": [
+            900000,
+            1800000,
+            2700000,
+            3600000,
+            4500000,
+            5400000,
+            6300000,
+            7200000,
+            8100000,
+            9000000
+        ]
+    },
+    {
+        "schedule": "42 8,20 * * *",
+        "triggerTimes": [
+            31320000,
+            74520000,
+            117720000,
+            160920000,
+            204120000,
+            247320000,
+            290520000,
+            333720000,
+            376920000,
+            420120000
+        ]
+    },
+    {
+        "schedule": "46 */6 * * *",
+        "triggerTimes": [
+            2760000,
+            24360000,
+            45960000,
+            67560000,
+            89160000,
+            110760000,
+            132360000,
+            153960000,
+            175560000,
+            197160000
+        ]
+    },
+    {
+        "schedule": "0 3 * * *",
+        "triggerTimes": [
+            10800000,
+            97200000,
+            183600000,
+            270000000,
+            356400000,
+            442800000,
+            529200000,
+            615600000,
+            702000000,
+            788400000
+        ]
+    },
+    {
+        "schedule": "16 9,16 * * *",
+        "triggerTimes": [
+            33360000,
+            58560000,
+            119760000,
+            144960000,
+            206160000,
+            231360000,
+            292560000,
+            317760000,
+            378960000,
+            404160000
+        ]
+    },
+    {
+        "schedule": "15 0 * * *",
+        "triggerTimes": [
+            900000,
+            87300000,
+            173700000,
+            260100000,
+            346500000,
+            432900000,
+            519300000,
+            605700000,
+            692100000,
+            778500000
+        ]
+    },
+    {
+        "schedule": "05 * * * *",
+        "triggerTimes": [
+            300000,
+            3900000,
+            7500000,
+            11100000,
+            14700000,
+            18300000,
+            21900000,
+            25500000,
+            29100000,
+            32700000
+        ]
+    },
+    {
+        "schedule": "30 * * * *",
+        "triggerTimes": [
+            1800000,
+            5400000,
+            9000000,
+            12600000,
+            16200000,
+            19800000,
+            23400000,
+            27000000,
+            30600000,
+            34200000
+        ]
+    },
+    {
+        "schedule": "0 2,14 * * *",
+        "triggerTimes": [
+            7200000,
+            50400000,
+            93600000,
+            136800000,
+            180000000,
+            223200000,
+            266400000,
+            309600000,
+            352800000,
+            396000000
+        ]
+    },
+    {
+        "schedule": "28 23 * * 3",
+        "triggerTimes": [
+            602880000,
+            1207680000,
+            1812480000,
+            2417280000,
+            3022080000,
+            3626880000,
+            4231680000,
+            4836480000,
+            5441280000,
+            6046080000
+        ]
+    },
+    {
+        "schedule": "5 */4 * * *",
+        "triggerTimes": [
+            300000,
+            14700000,
+            29100000,
+            43500000,
+            57900000,
+            72300000,
+            86700000,
+            101100000,
+            115500000,
+            129900000
+        ]
+    },
+    {
+        "schedule": "0 18,22 * * MON-FRI",
+        "triggerTimes": [
+            64800000,
+            79200000,
+            151200000,
+            165600000,
+            410400000,
+            424800000,
+            496800000,
+            511200000,
+            583200000,
+            597600000
+        ]
+    },
+    {
+        "schedule": "01 21 * * *",
+        "triggerTimes": [
+            75660000,
+            162060000,
+            248460000,
+            334860000,
+            421260000,
+            507660000,
+            594060000,
+            680460000,
+            766860000,
+            853260000
+        ]
+    },
+    {
+        "schedule": "1 */6 * * *",
+        "triggerTimes": [
+            60000,
+            21660000,
+            43260000,
+            64860000,
+            86460000,
+            108060000,
+            129660000,
+            151260000,
+            172860000,
+            194460000
+        ]
+    },
+    {
+        "schedule": "*/10 * * * *",
+        "triggerTimes": [
+            600000,
+            1200000,
+            1800000,
+            2400000,
+            3000000,
+            3600000,
+            4200000,
+            4800000,
+            5400000,
+            6000000
+        ]
+    },
+    {
+        "schedule": "44    */2 *   *   *",
+        "triggerTimes": [
+            2640000,
+            9840000,
+            17040000,
+            24240000,
+            31440000,
+            38640000,
+            45840000,
+            53040000,
+            60240000,
+            67440000
+        ]
+    },
+    {
+        "schedule": "30 2 * * *",
+        "triggerTimes": [
+            9000000,
+            95400000,
+            181800000,
+            268200000,
+            354600000,
+            441000000,
+            527400000,
+            613800000,
+            700200000,
+            786600000
+        ]
+    },
+    {
+        "schedule": "58 * * * *",
+        "triggerTimes": [
+            3480000,
+            7080000,
+            10680000,
+            14280000,
+            17880000,
+            21480000,
+            25080000,
+            28680000,
+            32280000,
+            35880000
+        ]
+    },
+    {
+        "schedule": "30 23 * * 6",
+        "triggerTimes": [
+            257400000,
+            862200000,
+            1467000000,
+            2071800000,
+            2676600000,
+            3281400000,
+            3886200000,
+            4491000000,
+            5095800000,
+            5700600000
+        ]
+    },
+    {
+        "schedule": "40 23 * * *",
+        "triggerTimes": [
+            85200000,
+            171600000,
+            258000000,
+            344400000,
+            430800000,
+            517200000,
+            603600000,
+            690000000,
+            776400000,
+            862800000
+        ]
+    },
+    {
+        "schedule": "0 5,10,15,20,1 * * *",
+        "triggerTimes": [
+            3600000,
+            18000000,
+            36000000,
+            54000000,
+            72000000,
+            90000000,
+            104400000,
+            122400000,
+            140400000,
+            158400000
+        ]
+    },
+    {
+        "schedule": "22 * * * *",
+        "triggerTimes": [
+            1320000,
+            4920000,
+            8520000,
+            12120000,
+            15720000,
+            19320000,
+            22920000,
+            26520000,
+            30120000,
+            33720000
+        ]
+    },
+    {
+        "schedule": "00 17 1-3,5-31 * *",
+        "triggerTimes": [
+            61200000,
+            147600000,
+            234000000,
+            406800000,
+            493200000,
+            579600000,
+            666000000,
+            752400000,
+            838800000,
+            925200000
+        ]
+    },
+    {
+        "schedule": "0 2 1 * *",
+        "triggerTimes": [
+            7200000,
+            2685600000,
+            5104800000,
+            7783200000,
+            10375200000,
+            13053600000,
+            15645600000,
+            18324000000,
+            21002400000,
+            23594400000
+        ]
+    },
+    {
+        "schedule": "20 20 * * *",
+        "triggerTimes": [
+            73200000,
+            159600000,
+            246000000,
+            332400000,
+            418800000,
+            505200000,
+            591600000,
+            678000000,
+            764400000,
+            850800000
+        ]
+    },
+    {
+        "schedule": "45 1 * * *",
+        "triggerTimes": [
+            6300000,
+            92700000,
+            179100000,
+            265500000,
+            351900000,
+            438300000,
+            524700000,
+            611100000,
+            697500000,
+            783900000
+        ]
+    },
+    {
+        "schedule": "3-59/5 * * * *",
+        "triggerTimes": [
+            180000,
+            480000,
+            780000,
+            1080000,
+            1380000,
+            1680000,
+            1980000,
+            2280000,
+            2580000,
+            2880000
+        ]
+    },
+    {
+        "schedule": "21    *   *   *   *",
+        "triggerTimes": [
+            1260000,
+            4860000,
+            8460000,
+            12060000,
+            15660000,
+            19260000,
+            22860000,
+            26460000,
+            30060000,
+            33660000
+        ]
+    },
+    {
+        "schedule": "37 */1 * * *",
+        "triggerTimes": [
+            2220000,
+            5820000,
+            9420000,
+            13020000,
+            16620000,
+            20220000,
+            23820000,
+            27420000,
+            31020000,
+            34620000
+        ]
+    },
+    {
+        "schedule": "12 3 * * 1,3,5",
+        "triggerTimes": [
+            97920000,
+            357120000,
+            529920000,
+            702720000,
+            961920000,
+            1134720000,
+            1307520000,
+            1566720000,
+            1739520000,
+            1912320000
+        ]
+    },
+    {
+        "schedule": "10 * * * *",
+        "triggerTimes": [
+            600000,
+            4200000,
+            7800000,
+            11400000,
+            15000000,
+            18600000,
+            22200000,
+            25800000,
+            29400000,
+            33000000
+        ]
+    },
+    {
+        "schedule": "*/4 * * * *",
+        "triggerTimes": [
+            240000,
+            480000,
+            720000,
+            960000,
+            1200000,
+            1440000,
+            1680000,
+            1920000,
+            2160000,
+            2400000
+        ]
+    },
+    {
+        "schedule": "36 * * * *",
+        "triggerTimes": [
+            2160000,
+            5760000,
+            9360000,
+            12960000,
+            16560000,
+            20160000,
+            23760000,
+            27360000,
+            30960000,
+            34560000
+        ]
+    },
+    {
+        "schedule": "10 7 * * *",
+        "triggerTimes": [
+            25800000,
+            112200000,
+            198600000,
+            285000000,
+            371400000,
+            457800000,
+            544200000,
+            630600000,
+            717000000,
+            803400000
+        ]
+    },
+    {
+        "schedule": "55 6 * * *",
+        "triggerTimes": [
+            24900000,
+            111300000,
+            197700000,
+            284100000,
+            370500000,
+            456900000,
+            543300000,
+            629700000,
+            716100000,
+            802500000
+        ]
+    },
+    {
+        "schedule": "0 */2 * * *",
+        "triggerTimes": [
+            7200000,
+            14400000,
+            21600000,
+            28800000,
+            36000000,
+            43200000,
+            50400000,
+            57600000,
+            64800000,
+            72000000
+        ]
+    },
+    {
+        "schedule": "0 5 * * *",
+        "triggerTimes": [
+            18000000,
+            104400000,
+            190800000,
+            277200000,
+            363600000,
+            450000000,
+            536400000,
+            622800000,
+            709200000,
+            795600000
+        ]
+    },
+    {
+        "schedule": "22 */4 * * *",
+        "triggerTimes": [
+            1320000,
+            15720000,
+            30120000,
+            44520000,
+            58920000,
+            73320000,
+            87720000,
+            102120000,
+            116520000,
+            130920000
+        ]
+    },
+    {
+        "schedule": "17 */2 * * *",
+        "triggerTimes": [
+            1020000,
+            8220000,
+            15420000,
+            22620000,
+            29820000,
+            37020000,
+            44220000,
+            51420000,
+            58620000,
+            65820000
+        ]
+    },
+    {
+        "schedule": "25    *   *   *   *",
+        "triggerTimes": [
+            1500000,
+            5100000,
+            8700000,
+            12300000,
+            15900000,
+            19500000,
+            23100000,
+            26700000,
+            30300000,
+            33900000
+        ]
+    },
+    {
+        "schedule": "*/6 * * * *",
+        "triggerTimes": [
+            360000,
+            720000,
+            1080000,
+            1440000,
+            1800000,
+            2160000,
+            2520000,
+            2880000,
+            3240000,
+            3600000
+        ]
+    },
+    {
+        "schedule": "5 * * * *",
+        "triggerTimes": [
+            300000,
+            3900000,
+            7500000,
+            11100000,
+            14700000,
+            18300000,
+            21900000,
+            25500000,
+            29100000,
+            32700000
+        ]
+    },
+    {
+        "schedule": "0 2 * * *",
+        "triggerTimes": [
+            7200000,
+            93600000,
+            180000000,
+            266400000,
+            352800000,
+            439200000,
+            525600000,
+            612000000,
+            698400000,
+            784800000
+        ]
+    },
+    {
+        "schedule": "0     *   *   *   *",
+        "triggerTimes": [
+            3600000,
+            7200000,
+            10800000,
+            14400000,
+            18000000,
+            21600000,
+            25200000,
+            28800000,
+            32400000,
+            36000000
+        ]
+    },
+    {
+        "schedule": "0 14 * * *,,",
+        "triggerTimes": [
+            50400000,
+            136800000,
+            223200000,
+            309600000,
+            396000000,
+            482400000,
+            568800000,
+            655200000,
+            741600000,
+            828000000
+        ]
+    },
+    {
+        "schedule": "30 02,08,12 * * *",
+        "triggerTimes": [
+            9000000,
+            30600000,
+            45000000,
+            95400000,
+            117000000,
+            131400000,
+            181800000,
+            203400000,
+            217800000,
+            268200000
+        ]
+    },
+    {
+        "schedule": "44 23 * * *",
+        "triggerTimes": [
+            85440000,
+            171840000,
+            258240000,
+            344640000,
+            431040000,
+            517440000,
+            603840000,
+            690240000,
+            776640000,
+            863040000
+        ]
+    },
+    {
+        "schedule": "0 */4 * * *",
+        "triggerTimes": [
+            14400000,
+            28800000,
+            43200000,
+            57600000,
+            72000000,
+            86400000,
+            100800000,
+            115200000,
+            129600000,
+            144000000
+        ]
+    },
+    {
+        "schedule": "0 12 * * *",
+        "triggerTimes": [
+            43200000,
+            129600000,
+            216000000,
+            302400000,
+            388800000,
+            475200000,
+            561600000,
+            648000000,
+            734400000,
+            820800000
+        ]
+    },
+    {
+        "schedule": "*/2   *   *   *   *",
+        "triggerTimes": [
+            120000,
+            240000,
+            360000,
+            480000,
+            600000,
+            720000,
+            840000,
+            960000,
+            1080000,
+            1200000
+        ]
+    },
+    {
+        "schedule": "22    1   *   *   *",
+        "triggerTimes": [
+            4920000,
+            91320000,
+            177720000,
+            264120000,
+            350520000,
+            436920000,
+            523320000,
+            609720000,
+            696120000,
+            782520000
+        ]
+    },
+    {
+        "schedule": "45 * * * *",
+        "triggerTimes": [
+            2700000,
+            6300000,
+            9900000,
+            13500000,
+            17100000,
+            20700000,
+            24300000,
+            27900000,
+            31500000,
+            35100000
+        ]
+    },
+    {
+        "schedule": "00 23 * * *",
+        "triggerTimes": [
+            82800000,
+            169200000,
+            255600000,
+            342000000,
+            428400000,
+            514800000,
+            601200000,
+            687600000,
+            774000000,
+            860400000
+        ]
+    },
+    {
+        "schedule": "3,6,9,12,18,21,24,27,33,36,39,42,48,51,54,57 * * * *",
+        "triggerTimes": [
+            180000,
+            360000,
+            540000,
+            720000,
+            1080000,
+            1260000,
+            1440000,
+            1620000,
+            1980000,
+            2160000
+        ]
+    },
+    {
+        "schedule": "32    1   *   *   *",
+        "triggerTimes": [
+            5520000,
+            91920000,
+            178320000,
+            264720000,
+            351120000,
+            437520000,
+            523920000,
+            610320000,
+            696720000,
+            783120000
+        ]
+    },
+    {
+        "schedule": "35 */2 * * *",
+        "triggerTimes": [
+            2100000,
+            9300000,
+            16500000,
+            23700000,
+            30900000,
+            38100000,
+            45300000,
+            52500000,
+            59700000,
+            66900000
+        ]
+    },
+    {
+        "schedule": "27    1   *   *   *",
+        "triggerTimes": [
+            5220000,
+            91620000,
+            178020000,
+            264420000,
+            350820000,
+            437220000,
+            523620000,
+            610020000,
+            696420000,
+            782820000
+        ]
+    },
+    {
+        "schedule": "0 21 * * 3",
+        "triggerTimes": [
+            594000000,
+            1198800000,
+            1803600000,
+            2408400000,
+            3013200000,
+            3618000000,
+            4222800000,
+            4827600000,
+            5432400000,
+            6037200000
+        ]
+    },
+    {
+        "schedule": "55 03 * * *",
+        "triggerTimes": [
+            14100000,
+            100500000,
+            186900000,
+            273300000,
+            359700000,
+            446100000,
+            532500000,
+            618900000,
+            705300000,
+            791700000
+        ]
+    },
+    {
+        "schedule": "0 23 2-31 * *",
+        "triggerTimes": [
+            169200000,
+            255600000,
+            342000000,
+            428400000,
+            514800000,
+            601200000,
+            687600000,
+            774000000,
+            860400000,
+            946800000
+        ]
+    },
+    {
+        "schedule": "09 11 * * *",
+        "triggerTimes": [
+            40140000,
+            126540000,
+            212940000,
+            299340000,
+            385740000,
+            472140000,
+            558540000,
+            644940000,
+            731340000,
+            817740000
+        ]
+    },
+    {
+        "schedule": "0 14 * * *",
+        "triggerTimes": [
+            50400000,
+            136800000,
+            223200000,
+            309600000,
+            396000000,
+            482400000,
+            568800000,
+            655200000,
+            741600000,
+            828000000
+        ]
+    },
+    {
+        "schedule": "20 2,12,22 * * *",
+        "triggerTimes": [
+            8400000,
+            44400000,
+            80400000,
+            94800000,
+            130800000,
+            166800000,
+            181200000,
+            217200000,
+            253200000,
+            267600000
+        ]
+    },
+    {
+        "schedule": "2,6,10,14,18,22,26,30,34,38,42,46,50,54,58 * * * *",
+        "triggerTimes": [
+            120000,
+            360000,
+            600000,
+            840000,
+            1080000,
+            1320000,
+            1560000,
+            1800000,
+            2040000,
+            2280000
+        ]
+    },
+    {
+        "schedule": "1 16,18,20 * * *",
+        "triggerTimes": [
+            57660000,
+            64860000,
+            72060000,
+            144060000,
+            151260000,
+            158460000,
+            230460000,
+            237660000,
+            244860000,
+            316860000
+        ]
+    },
+    {
+        "schedule": "30 */6 * * *",
+        "triggerTimes": [
+            1800000,
+            23400000,
+            45000000,
+            66600000,
+            88200000,
+            109800000,
+            131400000,
+            153000000,
+            174600000,
+            196200000
+        ]
+    },
+    {
+        "schedule": "00 06,15 * * *",
+        "triggerTimes": [
+            21600000,
+            54000000,
+            108000000,
+            140400000,
+            194400000,
+            226800000,
+            280800000,
+            313200000,
+            367200000,
+            399600000
+        ]
+    },
+    {
+        "schedule": "52 4,10,16,22 * * *",
+        "triggerTimes": [
+            17520000,
+            39120000,
+            60720000,
+            82320000,
+            103920000,
+            125520000,
+            147120000,
+            168720000,
+            190320000,
+            211920000
+        ]
+    },
+    {
+        "schedule": "37    1   *   *   *",
+        "triggerTimes": [
+            5820000,
+            92220000,
+            178620000,
+            265020000,
+            351420000,
+            437820000,
+            524220000,
+            610620000,
+            697020000,
+            783420000
+        ]
+    },
+    {
+        "schedule": "10 10,14 * * *",
+        "triggerTimes": [
+            36600000,
+            51000000,
+            123000000,
+            137400000,
+            209400000,
+            223800000,
+            295800000,
+            310200000,
+            382200000,
+            396600000
+        ]
+    },
+    {
+        "schedule": "2,7,12,17,22,27,32,37,42,47,52,57 * * * *",
+        "triggerTimes": [
+            120000,
+            420000,
+            720000,
+            1020000,
+            1320000,
+            1620000,
+            1920000,
+            2220000,
+            2520000,
+            2820000
+        ]
+    },
+    {
+        "schedule": "0 21 * * *",
+        "triggerTimes": [
+            75600000,
+            162000000,
+            248400000,
+            334800000,
+            421200000,
+            507600000,
+            594000000,
+            680400000,
+            766800000,
+            853200000
+        ]
+    },
+    {
+        "schedule": "25 * * * *",
+        "triggerTimes": [
+            1500000,
+            5100000,
+            8700000,
+            12300000,
+            15900000,
+            19500000,
+            23100000,
+            26700000,
+            30300000,
+            33900000
+        ]
+    },
+    {
+        "schedule": "0 15 * * *,,",
+        "triggerTimes": [
+            54000000,
+            140400000,
+            226800000,
+            313200000,
+            399600000,
+            486000000,
+            572400000,
+            658800000,
+            745200000,
+            831600000
+        ]
+    },
+    {
+        "schedule": "13 9,21 * * *",
+        "triggerTimes": [
+            33180000,
+            76380000,
+            119580000,
+            162780000,
+            205980000,
+            249180000,
+            292380000,
+            335580000,
+            378780000,
+            421980000
+        ]
+    },
+    {
+        "schedule": "10    *   *   *   *",
+        "triggerTimes": [
+            600000,
+            4200000,
+            7800000,
+            11400000,
+            15000000,
+            18600000,
+            22200000,
+            25800000,
+            29400000,
+            33000000
+        ]
+    },
+    {
+        "schedule": "12 18 * * 1,3,5",
+        "triggerTimes": [
+            151920000,
+            411120000,
+            583920000,
+            756720000,
+            1015920000,
+            1188720000,
+            1361520000,
+            1620720000,
+            1793520000,
+            1966320000
+        ]
+    },
+    {
+        "schedule": "0 17-19 * * 1",
+        "triggerTimes": [
+            406800000,
+            410400000,
+            414000000,
+            1011600000,
+            1015200000,
+            1018800000,
+            1616400000,
+            1620000000,
+            1623600000,
+            2221200000
+        ]
+    },
+    {
+        "schedule": "0 10 * * *",
+        "triggerTimes": [
+            36000000,
+            122400000,
+            208800000,
+            295200000,
+            381600000,
+            468000000,
+            554400000,
+            640800000,
+            727200000,
+            813600000
+        ]
+    },
+    {
+        "schedule": "00 00 * * *",
+        "triggerTimes": [
+            86400000,
+            172800000,
+            259200000,
+            345600000,
+            432000000,
+            518400000,
+            604800000,
+            691200000,
+            777600000,
+            864000000
+        ]
+    },
+    {
+        "schedule": "25 16,17,18,22 * * *",
+        "triggerTimes": [
+            59100000,
+            62700000,
+            66300000,
+            80700000,
+            145500000,
+            149100000,
+            152700000,
+            167100000,
+            231900000,
+            235500000
+        ]
+    },
+    {
+        "schedule": "23 6,18 * * *",
+        "triggerTimes": [
+            22980000,
+            66180000,
+            109380000,
+            152580000,
+            195780000,
+            238980000,
+            282180000,
+            325380000,
+            368580000,
+            411780000
+        ]
+    },
+    {
+        "schedule": "17 1,9,17 * * 0",
+        "triggerTimes": [
+            263820000,
+            292620000,
+            321420000,
+            868620000,
+            897420000,
+            926220000,
+            1473420000,
+            1502220000,
+            1531020000,
+            2078220000
+        ]
+    },
+    {
+        "schedule": "00 16 * * *",
+        "triggerTimes": [
+            57600000,
+            144000000,
+            230400000,
+            316800000,
+            403200000,
+            489600000,
+            576000000,
+            662400000,
+            748800000,
+            835200000
+        ]
+    },
+    {
+        "schedule": "*/3 * * * *",
+        "triggerTimes": [
+            180000,
+            360000,
+            540000,
+            720000,
+            900000,
+            1080000,
+            1260000,
+            1440000,
+            1620000,
+            1800000
+        ]
+    },
+    {
+        "schedule": "19    *   *   *   *",
+        "triggerTimes": [
+            1140000,
+            4740000,
+            8340000,
+            11940000,
+            15540000,
+            19140000,
+            22740000,
+            26340000,
+            29940000,
+            33540000
+        ]
+    },
+    {
+        "schedule": "15 * * * *",
+        "triggerTimes": [
+            900000,
+            4500000,
+            8100000,
+            11700000,
+            15300000,
+            18900000,
+            22500000,
+            26100000,
+            29700000,
+            33300000
+        ]
+    },
+    {
+        "schedule": "*/15  *   *   *   *",
+        "triggerTimes": [
+            900000,
+            1800000,
+            2700000,
+            3600000,
+            4500000,
+            5400000,
+            6300000,
+            7200000,
+            8100000,
+            9000000
+        ]
+    },
+    {
+        "schedule": "0 22 * * 1",
+        "triggerTimes": [
+            424800000,
+            1029600000,
+            1634400000,
+            2239200000,
+            2844000000,
+            3448800000,
+            4053600000,
+            4658400000,
+            5263200000,
+            5868000000
+        ]
+    },
+    {
+        "schedule": "15    *   *   *   *",
+        "triggerTimes": [
+            900000,
+            4500000,
+            8100000,
+            11700000,
+            15300000,
+            18900000,
+            22500000,
+            26100000,
+            29700000,
+            33300000
+        ]
+    },
+    {
+        "schedule": "20 04 * * *",
+        "triggerTimes": [
+            15600000,
+            102000000,
+            188400000,
+            274800000,
+            361200000,
+            447600000,
+            534000000,
+            620400000,
+            706800000,
+            793200000
+        ]
+    },
+    {
+        "schedule": "30 0,12 * * *",
+        "triggerTimes": [
+            1800000,
+            45000000,
+            88200000,
+            131400000,
+            174600000,
+            217800000,
+            261000000,
+            304200000,
+            347400000,
+            390600000
+        ]
+    },
+    {
+        "schedule": "15 */4 * * *",
+        "triggerTimes": [
+            900000,
+            15300000,
+            29700000,
+            44100000,
+            58500000,
+            72900000,
+            87300000,
+            101700000,
+            116100000,
+            130500000
+        ]
+    },
+    {
+        "schedule": "29 16,17,18,22 * * *",
+        "triggerTimes": [
+            59340000,
+            62940000,
+            66540000,
+            80940000,
+            145740000,
+            149340000,
+            152940000,
+            167340000,
+            232140000,
+            235740000
+        ]
+    },
+    {
+        "schedule": "37 */3 * * *",
+        "triggerTimes": [
+            2220000,
+            13020000,
+            23820000,
+            34620000,
+            45420000,
+            56220000,
+            67020000,
+            77820000,
+            88620000,
+            99420000
+        ]
+    },
+    {
+        "schedule": "*/15 * * * *",
+        "triggerTimes": [
+            900000,
+            1800000,
+            2700000,
+            3600000,
+            4500000,
+            5400000,
+            6300000,
+            7200000,
+            8100000,
+            9000000
+        ]
+    },
+    {
+        "schedule": "35 23 * * *",
+        "triggerTimes": [
+            84900000,
+            171300000,
+            257700000,
+            344100000,
+            430500000,
+            516900000,
+            603300000,
+            689700000,
+            776100000,
+            862500000
+        ]
+    },
+    {
+        "schedule": "0 17 * * *",
+        "triggerTimes": [
+            61200000,
+            147600000,
+            234000000,
+            320400000,
+            406800000,
+            493200000,
+            579600000,
+            666000000,
+            752400000,
+            838800000
+        ]
+    },
+    {
+        "schedule": "0 22 * * *",
+        "triggerTimes": [
+            79200000,
+            165600000,
+            252000000,
+            338400000,
+            424800000,
+            511200000,
+            597600000,
+            684000000,
+            770400000,
+            856800000
+        ]
+    },
+    {
+        "schedule": "0 11 * * *",
+        "triggerTimes": [
+            39600000,
+            126000000,
+            212400000,
+            298800000,
+            385200000,
+            471600000,
+            558000000,
+            644400000,
+            730800000,
+            817200000
+        ]
+    },
+    {
+        "schedule": "30    *   *   *   *",
+        "triggerTimes": [
+            1800000,
+            5400000,
+            9000000,
+            12600000,
+            16200000,
+            19800000,
+            23400000,
+            27000000,
+            30600000,
+            34200000
+        ]
+    },
+    {
+        "schedule": "41 * * * *",
+        "triggerTimes": [
+            2460000,
+            6060000,
+            9660000,
+            13260000,
+            16860000,
+            20460000,
+            24060000,
+            27660000,
+            31260000,
+            34860000
+        ]
+    },
+    {
+        "schedule": "45 23 * * *",
+        "triggerTimes": [
+            85500000,
+            171900000,
+            258300000,
+            344700000,
+            431100000,
+            517500000,
+            603900000,
+            690300000,
+            776700000,
+            863100000
+        ]
+    },
+    {
+        "schedule": "*/2 * * * *",
+        "triggerTimes": [
+            120000,
+            240000,
+            360000,
+            480000,
+            600000,
+            720000,
+            840000,
+            960000,
+            1080000,
+            1200000
+        ]
+    },
+    {
+        "schedule": "0 0,3,6,9,12,15,18,21 * * *",
+        "triggerTimes": [
+            10800000,
+            21600000,
+            32400000,
+            43200000,
+            54000000,
+            64800000,
+            75600000,
+            86400000,
+            97200000,
+            108000000
+        ]
+    },
+    {
+        "schedule": "0,30 * * * *",
+        "triggerTimes": [
+            1800000,
+            3600000,
+            5400000,
+            7200000,
+            9000000,
+            10800000,
+            12600000,
+            14400000,
+            16200000,
+            18000000
+        ]
+    },
+    {
+        "schedule": "17    *   *   *   *",
+        "triggerTimes": [
+            1020000,
+            4620000,
+            8220000,
+            11820000,
+            15420000,
+            19020000,
+            22620000,
+            26220000,
+            29820000,
+            33420000
+        ]
+    },
+    {
+        "schedule": "30,45 18 * * 1",
+        "triggerTimes": [
+            412200000,
+            413100000,
+            1017000000,
+            1017900000,
+            1621800000,
+            1622700000,
+            2226600000,
+            2227500000,
+            2831400000,
+            2832300000
+        ]
+    },
+    {
+        "schedule": "13,43 * * * *",
+        "triggerTimes": [
+            780000,
+            2580000,
+            4380000,
+            6180000,
+            7980000,
+            9780000,
+            11580000,
+            13380000,
+            15180000,
+            16980000
+        ]
+    },
+    {
+        "schedule": "0 0 10 * *",
+        "triggerTimes": [
+            777600000,
+            3456000000,
+            5875200000,
+            8553600000,
+            11145600000,
+            13824000000,
+            16416000000,
+            19094400000,
+            21772800000,
+            24364800000
+        ]
+    },
+    {
+        "schedule": "13,28,43,58 * * * *",
+        "triggerTimes": [
+            780000,
+            1680000,
+            2580000,
+            3480000,
+            4380000,
+            5280000,
+            6180000,
+            7080000,
+            7980000,
+            8880000
+        ]
+    },
+    {
+        "schedule": "17 9,13,22 * * *",
+        "triggerTimes": [
+            33420000,
+            47820000,
+            80220000,
+            119820000,
+            134220000,
+            166620000,
+            206220000,
+            220620000,
+            253020000,
+            292620000
+        ]
+    },
+    {
+        "schedule": "10 8,12 * * *",
+        "triggerTimes": [
+            29400000,
+            43800000,
+            115800000,
+            130200000,
+            202200000,
+            216600000,
+            288600000,
+            303000000,
+            375000000,
+            389400000
+        ]
+    },
+    {
+        "schedule": "*/5   *   *   *   *",
+        "triggerTimes": [
+            300000,
+            600000,
+            900000,
+            1200000,
+            1500000,
+            1800000,
+            2100000,
+            2400000,
+            2700000,
+            3000000
+        ]
+    },
+    {
+        "schedule": "5,20,35,50 * * * *",
+        "triggerTimes": [
+            300000,
+            1200000,
+            2100000,
+            3000000,
+            3900000,
+            4800000,
+            5700000,
+            6600000,
+            7500000,
+            8400000
+        ]
+    },
+    {
+        "schedule": "00 */2 * * *",
+        "triggerTimes": [
+            7200000,
+            14400000,
+            21600000,
+            28800000,
+            36000000,
+            43200000,
+            50400000,
+            57600000,
+            64800000,
+            72000000
+        ]
+    },
+    {
+        "schedule": "23    *   *   *   *",
+        "triggerTimes": [
+            1380000,
+            4980000,
+            8580000,
+            12180000,
+            15780000,
+            19380000,
+            22980000,
+            26580000,
+            30180000,
+            33780000
+        ]
+    },
+    {
+        "schedule": "7 12 * * *",
+        "triggerTimes": [
+            43620000,
+            130020000,
+            216420000,
+            302820000,
+            389220000,
+            475620000,
+            562020000,
+            648420000,
+            734820000,
+            821220000
+        ]
+    },
+    {
+        "schedule": "*/1 * * * *",
+        "triggerTimes": [
+            60000,
+            120000,
+            180000,
+            240000,
+            300000,
+            360000,
+            420000,
+            480000,
+            540000,
+            600000
+        ]
+    },
+    {
+        "schedule": "0,10,20,30,40,50 * * * *",
+        "triggerTimes": [
+            600000,
+            1200000,
+            1800000,
+            2400000,
+            3000000,
+            3600000,
+            4200000,
+            4800000,
+            5400000,
+            6000000
+        ]
+    },
+    {
+        "schedule": "45 02,06,10,14,18,22 * * *",
+        "triggerTimes": [
+            9900000,
+            24300000,
+            38700000,
+            53100000,
+            67500000,
+            81900000,
+            96300000,
+            110700000,
+            125100000,
+            139500000
+        ]
+    },
+    {
+        "schedule": "39    1   *   *   *",
+        "triggerTimes": [
+            5940000,
+            92340000,
+            178740000,
+            265140000,
+            351540000,
+            437940000,
+            524340000,
+            610740000,
+            697140000,
+            783540000
+        ]
+    },
+    {
+        "schedule": "0 0-2 * * 2-6",
+        "triggerTimes": [
+            3600000,
+            7200000,
+            86400000,
+            90000000,
+            93600000,
+            172800000,
+            176400000,
+            180000000,
+            432000000,
+            435600000
+        ]
+    },
+    {
+        "schedule": "35,50 * * * *",
+        "triggerTimes": [
+            2100000,
+            3000000,
+            5700000,
+            6600000,
+            9300000,
+            10200000,
+            12900000,
+            13800000,
+            16500000,
+            17400000
+        ]
+    },
+    {
+        "schedule": "0 3 1 * *",
+        "triggerTimes": [
+            10800000,
+            2689200000,
+            5108400000,
+            7786800000,
+            10378800000,
+            13057200000,
+            15649200000,
+            18327600000,
+            21006000000,
+            23598000000
+        ]
+    },
+    {
+        "schedule": "5 5 * * *",
+        "triggerTimes": [
+            18300000,
+            104700000,
+            191100000,
+            277500000,
+            363900000,
+            450300000,
+            536700000,
+            623100000,
+            709500000,
+            795900000
+        ]
+    },
+    {
+        "schedule": "18    8   *   *   *",
+        "triggerTimes": [
+            29880000,
+            116280000,
+            202680000,
+            289080000,
+            375480000,
+            461880000,
+            548280000,
+            634680000,
+            721080000,
+            807480000
+        ]
+    },
+    {
+        "schedule": "0 9 * * *",
+        "triggerTimes": [
+            32400000,
+            118800000,
+            205200000,
+            291600000,
+            378000000,
+            464400000,
+            550800000,
+            637200000,
+            723600000,
+            810000000
+        ]
+    },
+    {
+        "schedule": "*/1   *   *   *   *",
+        "triggerTimes": [
+            60000,
+            120000,
+            180000,
+            240000,
+            300000,
+            360000,
+            420000,
+            480000,
+            540000,
+            600000
+        ]
+    },
+    {
+        "schedule": "50 8,12,21 * * *",
+        "triggerTimes": [
+            31800000,
+            46200000,
+            78600000,
+            118200000,
+            132600000,
+            165000000,
+            204600000,
+            219000000,
+            251400000,
+            291000000
+        ]
+    },
+    {
+        "schedule": "29 9,21 * * *",
+        "triggerTimes": [
+            34140000,
+            77340000,
+            120540000,
+            163740000,
+            206940000,
+            250140000,
+            293340000,
+            336540000,
+            379740000,
+            422940000
+        ]
+    },
+    {
+        "schedule": "40 * * * *",
+        "triggerTimes": [
+            2400000,
+            6000000,
+            9600000,
+            13200000,
+            16800000,
+            20400000,
+            24000000,
+            27600000,
+            31200000,
+            34800000
+        ]
+    },
+    {
+        "schedule": "8 21 * * *",
+        "triggerTimes": [
+            76080000,
+            162480000,
+            248880000,
+            335280000,
+            421680000,
+            508080000,
+            594480000,
+            680880000,
+            767280000,
+            853680000
+        ]
+    },
+    {
+        "schedule": "0 6 * * *",
+        "triggerTimes": [
+            21600000,
+            108000000,
+            194400000,
+            280800000,
+            367200000,
+            453600000,
+            540000000,
+            626400000,
+            712800000,
+            799200000
+        ]
+    },
+    {
+        "schedule": "30 0-23/2 * * *",
+        "triggerTimes": [
+            1800000,
+            9000000,
+            16200000,
+            23400000,
+            30600000,
+            37800000,
+            45000000,
+            52200000,
+            59400000,
+            66600000
+        ]
+    },
+    {
+        "schedule": "0 14,22 * * *",
+        "triggerTimes": [
+            50400000,
+            79200000,
+            136800000,
+            165600000,
+            223200000,
+            252000000,
+            309600000,
+            338400000,
+            396000000,
+            424800000
+        ]
+    },
+    {
+        "schedule": "0 */1 * * *",
+        "triggerTimes": [
+            3600000,
+            7200000,
+            10800000,
+            14400000,
+            18000000,
+            21600000,
+            25200000,
+            28800000,
+            32400000,
+            36000000
+        ]
+    },
+    {
+        "schedule": "0 1 * * 1",
+        "triggerTimes": [
+            349200000,
+            954000000,
+            1558800000,
+            2163600000,
+            2768400000,
+            3373200000,
+            3978000000,
+            4582800000,
+            5187600000,
+            5792400000
+        ]
+    },
+    {
+        "schedule": "0 8 * * *",
+        "triggerTimes": [
+            28800000,
+            115200000,
+            201600000,
+            288000000,
+            374400000,
+            460800000,
+            547200000,
+            633600000,
+            720000000,
+            806400000
+        ]
+    },
+    {
+        "schedule": "01 17 * * *",
+        "triggerTimes": [
+            61260000,
+            147660000,
+            234060000,
+            320460000,
+            406860000,
+            493260000,
+            579660000,
+            666060000,
+            752460000,
+            838860000
+        ]
+    },
+    {
+        "schedule": "13    *   *   *   *",
+        "triggerTimes": [
+            780000,
+            4380000,
+            7980000,
+            11580000,
+            15180000,
+            18780000,
+            22380000,
+            25980000,
+            29580000,
+            33180000
+        ]
+    }
+]


[4/5] CronScheduler based on Quartz

Posted by ke...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/http/ServletModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/ServletModule.java b/src/main/java/org/apache/aurora/scheduler/http/ServletModule.java
index 9831012..6d76f60 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/ServletModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/ServletModule.java
@@ -39,8 +39,8 @@ import com.twitter.common.net.pool.DynamicHostSet;
 import com.twitter.common.net.pool.DynamicHostSet.MonitorException;
 import com.twitter.thrift.ServiceInstance;
 
+import org.apache.aurora.scheduler.cron.CronJobManager;
 import org.apache.aurora.scheduler.quota.QuotaManager;
-import org.apache.aurora.scheduler.state.CronJobManager;
 import org.apache.aurora.scheduler.state.SchedulerCore;
 import org.apache.aurora.scheduler.storage.entities.IServerInfo;
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/http/StructDump.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/StructDump.java b/src/main/java/org/apache/aurora/scheduler/http/StructDump.java
index efea75f..823668f 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/StructDump.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/StructDump.java
@@ -25,7 +25,6 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
 import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
 import com.twitter.common.base.Closure;
 import com.twitter.common.thrift.Util;
@@ -34,7 +33,7 @@ import org.antlr.stringtemplate.StringTemplate;
 import org.apache.aurora.gen.JobConfiguration;
 import org.apache.aurora.scheduler.base.JobKeys;
 import org.apache.aurora.scheduler.base.Query;
-import org.apache.aurora.scheduler.state.CronJobManager;
+import org.apache.aurora.scheduler.cron.CronJobManager;
 import org.apache.aurora.scheduler.storage.Storage;
 import org.apache.aurora.scheduler.storage.Storage.StoreProvider;
 import org.apache.aurora.scheduler.storage.Storage.Work;
@@ -43,6 +42,8 @@ import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
 import org.apache.aurora.scheduler.storage.entities.IJobKey;
 import org.apache.thrift.TBase;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 /**
  * Servlet that prints out the raw configuration for a specified struct.
  */
@@ -50,11 +51,13 @@ import org.apache.thrift.TBase;
 public class StructDump extends JerseyTemplateServlet {
 
   private final Storage storage;
+  private final CronJobManager cronJobManager;
 
   @Inject
-  public StructDump(Storage storage) {
+  public StructDump(Storage storage, CronJobManager cronJobManager) {
     super("structdump");
-    this.storage = Preconditions.checkNotNull(storage);
+    this.storage = checkNotNull(storage);
+    this.cronJobManager = checkNotNull(cronJobManager);
   }
 
   private static final String USAGE =
@@ -106,11 +109,11 @@ public class StructDump extends JerseyTemplateServlet {
       @PathParam("job") final String job) {
 
     final IJobKey jobKey = JobKeys.from(role, environment, job);
-    return dumpEntity("Cron job " + JobKeys.toPath(jobKey),
+    return dumpEntity("Cron job " + JobKeys.canonicalString(jobKey),
         new Work.Quiet<Optional<? extends TBase<?, ?>>>() {
           @Override
           public Optional<JobConfiguration> apply(StoreProvider storeProvider) {
-            return storeProvider.getJobStore().fetchJob(CronJobManager.MANAGER_KEY, jobKey)
+            return storeProvider.getJobStore().fetchJob(cronJobManager.getManagerKey(), jobKey)
                 .transform(IJobConfiguration.TO_BUILDER);
           }
         });

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/sla/SlaGroup.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/sla/SlaGroup.java b/src/main/java/org/apache/aurora/scheduler/sla/SlaGroup.java
index ff7e3cb..46be612 100644
--- a/src/main/java/org/apache/aurora/scheduler/sla/SlaGroup.java
+++ b/src/main/java/org/apache/aurora/scheduler/sla/SlaGroup.java
@@ -119,7 +119,7 @@ interface SlaGroup {
       return Multimaps.index(tasks, Functions.compose(new Function<IJobKey, String>() {
         @Override
         public String apply(IJobKey jobKey) {
-          return "sla_" + JobKeys.toPath(jobKey) + "_";
+          return "sla_" + JobKeys.canonicalString(jobKey) + "_";
         }
       }, Tasks.SCHEDULED_TO_JOB_KEY));
     }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/state/CronJobManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/state/CronJobManager.java b/src/main/java/org/apache/aurora/scheduler/state/CronJobManager.java
deleted file mode 100644
index 4bd190c..0000000
--- a/src/main/java/org/apache/aurora/scheduler/state/CronJobManager.java
+++ /dev/null
@@ -1,484 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.state;
-
-import java.util.Collections;
-import java.util.Date;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
-import com.google.common.eventbus.Subscribe;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.twitter.common.application.ShutdownRegistry;
-import com.twitter.common.args.Arg;
-import com.twitter.common.args.CmdLine;
-import com.twitter.common.base.Command;
-import com.twitter.common.base.Supplier;
-import com.twitter.common.quantity.Amount;
-import com.twitter.common.quantity.Time;
-import com.twitter.common.stats.Stats;
-import com.twitter.common.util.BackoffHelper;
-
-import org.apache.aurora.gen.CronCollisionPolicy;
-import org.apache.aurora.gen.ScheduleStatus;
-import org.apache.aurora.scheduler.base.JobKeys;
-import org.apache.aurora.scheduler.base.Query;
-import org.apache.aurora.scheduler.base.ScheduleException;
-import org.apache.aurora.scheduler.base.Tasks;
-import org.apache.aurora.scheduler.configuration.ConfigurationManager.TaskDescriptionException;
-import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
-import org.apache.aurora.scheduler.cron.CronException;
-import org.apache.aurora.scheduler.cron.CronScheduler;
-import org.apache.aurora.scheduler.events.PubsubEvent.EventSubscriber;
-import org.apache.aurora.scheduler.events.PubsubEvent.SchedulerActive;
-import org.apache.aurora.scheduler.storage.Storage;
-import org.apache.aurora.scheduler.storage.Storage.MutateWork;
-import org.apache.aurora.scheduler.storage.Storage.Work;
-import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
-import org.apache.aurora.scheduler.storage.entities.IJobKey;
-import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
-import org.apache.commons.lang.StringUtils;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import static org.apache.aurora.gen.ScheduleStatus.KILLING;
-
-/**
- * A job scheduler that receives jobs that should be run periodically on a cron schedule.
- */
-public class CronJobManager implements EventSubscriber {
-
-  public static final String MANAGER_KEY = "CRON";
-
-  @VisibleForTesting
-  static final Optional<String> KILL_AUDIT_MESSAGE = Optional.of("Killed by cron");
-
-  private static final Logger LOG = Logger.getLogger(CronJobManager.class.getName());
-
-  @CmdLine(name = "cron_start_initial_backoff", help =
-      "Initial backoff delay while waiting for a previous cron run to start.")
-  private static final Arg<Amount<Long, Time>> CRON_START_INITIAL_BACKOFF =
-      Arg.create(Amount.of(1L, Time.SECONDS));
-
-  @CmdLine(name = "cron_start_max_backoff", help =
-      "Max backoff delay while waiting for a previous cron run to start.")
-  private static final Arg<Amount<Long, Time>> CRON_START_MAX_BACKOFF =
-      Arg.create(Amount.of(1L, Time.MINUTES));
-
-  private final AtomicLong cronJobsTriggered = Stats.exportLong("cron_jobs_triggered");
-  private final AtomicLong cronJobLaunchFailures = Stats.exportLong("cron_job_launch_failures");
-
-  // Maps from the unique job identifier to the unique identifier used internally by the cron
-  // scheduler.
-  private final Map<IJobKey, String> scheduledJobs =
-      Collections.synchronizedMap(Maps.<IJobKey, String>newHashMap());
-
-  // Prevents runs from dogpiling while waiting for a run to transition out of the KILLING state.
-  // This is necessary because killing a job (if dictated by cron collision policy) is an
-  // asynchronous operation.
-  private final Map<IJobKey, SanitizedConfiguration> pendingRuns =
-      Collections.synchronizedMap(Maps.<IJobKey, SanitizedConfiguration>newHashMap());
-
-  private final StateManager stateManager;
-  private final Storage storage;
-  private final CronScheduler cron;
-  private final ShutdownRegistry shutdownRegistry;
-  private final BackoffHelper delayedStartBackoff;
-  private final Executor delayedRunExecutor;
-
-  @Inject
-  CronJobManager(
-      StateManager stateManager,
-      Storage storage,
-      CronScheduler cron,
-      ShutdownRegistry shutdownRegistry) {
-
-    this(
-        stateManager,
-        storage,
-        cron,
-        shutdownRegistry,
-        Executors.newCachedThreadPool(
-            new ThreadFactoryBuilder().setDaemon(true).setNameFormat("CronDelay-%d").build()));
-  }
-
-  @VisibleForTesting
-  CronJobManager(
-      StateManager stateManager,
-      Storage storage,
-      CronScheduler cron,
-      ShutdownRegistry shutdownRegistry,
-      Executor delayedRunExecutor) {
-
-    this.stateManager = checkNotNull(stateManager);
-    this.storage = checkNotNull(storage);
-    this.cron = checkNotNull(cron);
-    this.shutdownRegistry = checkNotNull(shutdownRegistry);
-    this.delayedStartBackoff =
-        new BackoffHelper(CRON_START_INITIAL_BACKOFF.get(), CRON_START_MAX_BACKOFF.get());
-    this.delayedRunExecutor = checkNotNull(delayedRunExecutor);
-
-    Stats.exportSize("cron_num_pending_runs", pendingRuns);
-  }
-
-  private void mapScheduledJob(SanitizedCronJob cronJob) throws ScheduleException {
-    IJobKey jobKey = cronJob.config.getJobConfig().getKey();
-    synchronized (scheduledJobs) {
-      Preconditions.checkState(
-          !scheduledJobs.containsKey(jobKey),
-          "Illegal state - cron schedule already exists for " + JobKeys.toPath(jobKey));
-      scheduledJobs.put(jobKey, scheduleJob(cronJob));
-    }
-  }
-
-  /**
-   * Notifies the cron job manager that the scheduler is active, and job configurations are ready to
-   * load.
-   *
-   * @param schedulerActive Event.
-   */
-  @Subscribe
-  public void schedulerActive(SchedulerActive schedulerActive) {
-    cron.startAsync().awaitRunning();
-    shutdownRegistry.addAction(new Command() {
-      @Override
-      public void execute() {
-        // NOTE: We don't know ahead-of-time which thread will execute the shutdown command,
-        // so we shouldn't block here.
-        cron.stopAsync();
-      }
-    });
-
-    Iterable<IJobConfiguration> crons =
-        storage.consistentRead(new Work.Quiet<Iterable<IJobConfiguration>>() {
-          @Override
-          public Iterable<IJobConfiguration> apply(Storage.StoreProvider storeProvider) {
-            return storeProvider.getJobStore().fetchJobs(MANAGER_KEY);
-          }
-        });
-
-    for (IJobConfiguration job : crons) {
-      try {
-        mapScheduledJob(new SanitizedCronJob(job, cron));
-      } catch (ScheduleException | TaskDescriptionException e) {
-        logLaunchFailure(job, e);
-      }
-    }
-  }
-
-  private void logLaunchFailure(IJobConfiguration job, Exception e) {
-    cronJobLaunchFailures.incrementAndGet();
-    LOG.log(Level.SEVERE, "Scheduling failed for recovered job " + job, e);
-  }
-
-  /**
-   * Triggers execution of a job.
-   *
-   * @param jobKey Key of the job to start.
-   * @throws ScheduleException If the job could not be started with the cron system.
-   * @throws TaskDescriptionException If the stored job associated with {@code jobKey} has field
-   *         validation problems.
-   */
-  public void startJobNow(IJobKey jobKey) throws TaskDescriptionException, ScheduleException {
-    Optional<IJobConfiguration> jobConfig = fetchJob(jobKey);
-    if (!jobConfig.isPresent()) {
-      throw new ScheduleException("Cron job does not exist for " + JobKeys.toPath(jobKey));
-    }
-
-    cronTriggered(new SanitizedCronJob(jobConfig.get(), cron));
-  }
-
-  private void delayedRun(final Query.Builder query, final SanitizedConfiguration config) {
-    IJobConfiguration job = config.getJobConfig();
-    final String jobPath = JobKeys.toPath(job);
-    final IJobKey jobKey = job.getKey();
-    LOG.info("Waiting for job to terminate before launching cron job " + jobPath);
-    if (pendingRuns.put(jobKey, config) == null) {
-      LOG.info("Launching a task to wait for job to finish: " + jobPath);
-      // There was no run already pending for this job, launch a task to delay launch until the
-      // existing run has terminated.
-      delayedRunExecutor.execute(new Runnable() {
-        @Override
-        public void run() {
-          runWhenTerminated(query, jobKey);
-        }
-      });
-    }
-  }
-
-  private void runWhenTerminated(final Query.Builder query, final IJobKey jobKey) {
-    try {
-      delayedStartBackoff.doUntilSuccess(new Supplier<Boolean>() {
-        @Override
-        public Boolean get() {
-          if (Storage.Util.consistentFetchTasks(storage, query).isEmpty()) {
-            LOG.info("Initiating delayed launch of cron " + jobKey);
-            SanitizedConfiguration config = pendingRuns.remove(jobKey);
-            checkNotNull(config, "Failed to fetch job for delayed run of " + jobKey);
-            LOG.info("Launching " + config.getTaskConfigs().size() + " tasks.");
-            stateManager.insertPendingTasks(config.getTaskConfigs());
-            return true;
-          } else {
-            LOG.info("Not yet safe to run cron " + jobKey);
-            return false;
-          }
-        }
-      });
-    } catch (InterruptedException e) {
-      LOG.log(Level.WARNING, "Interrupted while trying to launch cron " + jobKey, e);
-      Thread.currentThread().interrupt();
-    }
-  }
-
-  private void killActiveTasks(Set<String> taskIds) {
-    if (taskIds.isEmpty()) {
-      return;
-    }
-
-    for (String taskId : taskIds) {
-      stateManager.changeState(
-          taskId,
-          Optional.<ScheduleStatus>absent(),
-          KILLING,
-          KILL_AUDIT_MESSAGE);
-    }
-  }
-
-  public static CronCollisionPolicy orDefault(@Nullable CronCollisionPolicy policy) {
-    return Optional.fromNullable(policy).or(CronCollisionPolicy.KILL_EXISTING);
-  }
-
-  /**
-   * Triggers execution of a cron job, depending on the cron collision policy for the job.
-   *
-   * @param cronJob The job to be triggered.
-   */
-  private void cronTriggered(SanitizedCronJob cronJob) {
-    final SanitizedConfiguration config = cronJob.config;
-    final IJobConfiguration job = config.getJobConfig();
-    LOG.info(String.format("Cron triggered for %s at %s with policy %s",
-        JobKeys.toPath(job), new Date(), job.getCronCollisionPolicy()));
-    cronJobsTriggered.incrementAndGet();
-
-    storage.write(new MutateWork.NoResult.Quiet() {
-      @Override protected void execute(Storage.MutableStoreProvider storeProvider) {
-        ImmutableMap.Builder<Integer, ITaskConfig> builder = ImmutableMap.builder();
-        Query.Builder activeQuery = Query.jobScoped(job.getKey()).active();
-        Set<String> activeTasks = Tasks.ids(storeProvider.getTaskStore().fetchTasks(activeQuery));
-
-        if (activeTasks.isEmpty()) {
-          builder.putAll(config.getTaskConfigs());
-        } else {
-          switch (orDefault(job.getCronCollisionPolicy())) {
-            case KILL_EXISTING:
-              killActiveTasks(activeTasks);
-              delayedRun(activeQuery, config);
-              break;
-
-            case CANCEL_NEW:
-              break;
-
-            case RUN_OVERLAP:
-              LOG.severe(String.format(
-                  "Ignoring trigger for job %s with deprecated collision"
-                  + "policy RUN_OVERLAP due to unterminated active tasks.",
-                  JobKeys.toPath(job)));
-              break;
-
-            default:
-              LOG.severe("Unrecognized cron collision policy: " + job.getCronCollisionPolicy());
-          }
-        }
-
-        Map<Integer, ITaskConfig> newTasks = builder.build();
-        if (!newTasks.isEmpty()) {
-          stateManager.insertPendingTasks(newTasks);
-        }
-      }
-    });
-  }
-
-  /**
-   * Updates (re-schedules) the existing cron job.
-   *
-   * @param config New job configuration to update to.
-   * @throws ScheduleException If non-cron job confuration provided.
-   */
-  public void updateJob(SanitizedConfiguration config) throws ScheduleException {
-    IJobConfiguration job = config.getJobConfig();
-    if (!hasCronSchedule(job)) {
-      throw new ScheduleException("A cron job may not be updated to a non-cron job.");
-    }
-    String key = scheduledJobs.remove(job.getKey());
-    if (key == null) {
-      throw new ScheduleException(
-          "No cron template found for the given key: " + JobKeys.toPath(job));
-    }
-    cron.deschedule(key);
-    checkArgument(receiveJob(config));
-  }
-
-  private static boolean hasCronSchedule(IJobConfiguration job) {
-    checkNotNull(job);
-    return !StringUtils.isEmpty(job.getCronSchedule());
-  }
-
-  public boolean receiveJob(SanitizedConfiguration config) throws ScheduleException {
-    final IJobConfiguration job = config.getJobConfig();
-    if (!hasCronSchedule(job)) {
-      return false;
-    }
-
-    if (CronCollisionPolicy.RUN_OVERLAP.equals(job.getCronCollisionPolicy())) {
-      throw new ScheduleException(
-          "The RUN_OVERLAP collision policy has been removed (AURORA-38).");
-    }
-
-    SanitizedCronJob cronJob = new SanitizedCronJob(config, cron);
-    storage.write(new MutateWork.NoResult.Quiet() {
-      @Override
-      protected void execute(Storage.MutableStoreProvider storeProvider) {
-        storeProvider.getJobStore().saveAcceptedJob(MANAGER_KEY, job);
-      }
-    });
-    mapScheduledJob(cronJob);
-
-    return true;
-  }
-
-  private String scheduleJob(final SanitizedCronJob cronJob) throws ScheduleException {
-    IJobConfiguration job = cronJob.config.getJobConfig();
-    final String jobPath = JobKeys.toPath(job);
-    LOG.info(String.format("Scheduling cron job %s: %s", jobPath, job.getCronSchedule()));
-    try {
-      return cron.schedule(job.getCronSchedule(), new Runnable() {
-        @Override
-        public void run() {
-          // TODO(William Farner): May want to record information about job runs.
-          LOG.info("Running cron job: " + jobPath);
-          cronTriggered(cronJob);
-        }
-      });
-    } catch (CronException e) {
-      throw new ScheduleException("Failed to schedule cron job: " + e.getMessage(), e);
-    }
-  }
-
-  public Iterable<IJobConfiguration> getJobs() {
-    return storage.consistentRead(new Work.Quiet<Iterable<IJobConfiguration>>() {
-      @Override
-      public Iterable<IJobConfiguration> apply(Storage.StoreProvider storeProvider) {
-        return storeProvider.getJobStore().fetchJobs(MANAGER_KEY);
-      }
-    });
-  }
-
-  public boolean hasJob(IJobKey jobKey) {
-    return fetchJob(jobKey).isPresent();
-  }
-
-  private Optional<IJobConfiguration> fetchJob(final IJobKey jobKey) {
-    checkNotNull(jobKey);
-    return storage.consistentRead(new Work.Quiet<Optional<IJobConfiguration>>() {
-      @Override
-      public Optional<IJobConfiguration> apply(Storage.StoreProvider storeProvider) {
-        return storeProvider.getJobStore().fetchJob(MANAGER_KEY, jobKey);
-      }
-    });
-  }
-
-  public boolean deleteJob(final IJobKey jobKey) {
-    Optional<IJobConfiguration> job = fetchJob(jobKey);
-    if (!job.isPresent()) {
-      return false;
-    }
-
-    String scheduledJobKey = scheduledJobs.remove(jobKey);
-    if (scheduledJobKey != null) {
-      cron.deschedule(scheduledJobKey);
-      storage.write(new MutateWork.NoResult.Quiet() {
-        @Override
-        protected void execute(Storage.MutableStoreProvider storeProvider) {
-          storeProvider.getJobStore().removeJob(jobKey);
-        }
-      });
-      LOG.info("Successfully deleted cron job " + jobKey);
-    }
-    return true;
-  }
-
-  private final Function<String, String> keyToSchedule = new Function<String, String>() {
-    @Override
-    public String apply(String key) {
-      return cron.getSchedule(key).or("Not found.");
-    }
-  };
-
-  public Map<IJobKey, String> getScheduledJobs() {
-    synchronized (scheduledJobs) {
-      return ImmutableMap.copyOf(Maps.transformValues(scheduledJobs, keyToSchedule));
-    }
-  }
-
-  public Set<IJobKey> getPendingRuns() {
-    synchronized (pendingRuns) {
-      return ImmutableSet.copyOf(pendingRuns.keySet());
-    }
-  }
-
-  /**
-   * Used by functions that expect field validation before being called.
-   */
-  private static class SanitizedCronJob {
-    private final SanitizedConfiguration config;
-
-    SanitizedCronJob(IJobConfiguration unsanitized, CronScheduler cron)
-        throws ScheduleException, TaskDescriptionException {
-
-      this(SanitizedConfiguration.fromUnsanitized(unsanitized), cron);
-    }
-
-    SanitizedCronJob(SanitizedConfiguration config, CronScheduler cron) throws ScheduleException {
-      final IJobConfiguration job = config.getJobConfig();
-      if (!hasCronSchedule(job)) {
-        throw new ScheduleException(
-            String.format("Not a valid cronjob, %s has no cron schedule", JobKeys.toPath(job)));
-      }
-
-      if (!cron.isValidSchedule(job.getCronSchedule())) {
-        throw new ScheduleException("Invalid cron schedule: " + job.getCronSchedule());
-      }
-
-      this.config = config;
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/state/LockManagerImpl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/state/LockManagerImpl.java b/src/main/java/org/apache/aurora/scheduler/state/LockManagerImpl.java
index 5696485..e3b5b04 100644
--- a/src/main/java/org/apache/aurora/scheduler/state/LockManagerImpl.java
+++ b/src/main/java/org/apache/aurora/scheduler/state/LockManagerImpl.java
@@ -125,7 +125,7 @@ class LockManagerImpl implements LockManager {
   private static String formatLockKey(ILockKey lockKey) {
     switch (lockKey.getSetField()) {
       case JOB:
-        return JobKeys.toPath(lockKey.getJob());
+        return JobKeys.canonicalString(lockKey.getJob());
       default:
         return "Unknown lock key type: " + lockKey.getSetField();
     }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/state/SchedulerCoreImpl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/state/SchedulerCoreImpl.java b/src/main/java/org/apache/aurora/scheduler/state/SchedulerCoreImpl.java
index f330599..d377974 100644
--- a/src/main/java/org/apache/aurora/scheduler/state/SchedulerCoreImpl.java
+++ b/src/main/java/org/apache/aurora/scheduler/state/SchedulerCoreImpl.java
@@ -40,6 +40,9 @@ import org.apache.aurora.scheduler.base.Query;
 import org.apache.aurora.scheduler.base.ScheduleException;
 import org.apache.aurora.scheduler.base.Tasks;
 import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
 import org.apache.aurora.scheduler.quota.QuotaCheckResult;
 import org.apache.aurora.scheduler.quota.QuotaManager;
 import org.apache.aurora.scheduler.storage.Storage;
@@ -49,6 +52,7 @@ import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
 import org.apache.aurora.scheduler.storage.entities.IJobKey;
 import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
 import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
+import org.apache.commons.lang.StringUtils;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -71,7 +75,7 @@ class SchedulerCoreImpl implements SchedulerCore {
 
   // TODO(wfarner): Consider changing this class to not be concerned with cron jobs, requiring the
   // caller to deal with the fork.
-  private final CronJobManager cronScheduler;
+  private final CronJobManager cronJobManager;
 
   // State manager handles persistence of task modifications and state transitions.
   private final StateManager stateManager;
@@ -83,7 +87,7 @@ class SchedulerCoreImpl implements SchedulerCore {
    * Creates a new core scheduler.
    *
    * @param storage Backing store implementation.
-   * @param cronScheduler Cron scheduler.
+   * @param cronJobManager Cron scheduler.
    * @param stateManager Persistent state manager.
    * @param taskIdGenerator Task ID generator.
    * @param quotaManager Quota manager.
@@ -91,13 +95,13 @@ class SchedulerCoreImpl implements SchedulerCore {
   @Inject
   public SchedulerCoreImpl(
       Storage storage,
-      CronJobManager cronScheduler,
+      CronJobManager cronJobManager,
       StateManager stateManager,
       TaskIdGenerator taskIdGenerator,
       QuotaManager quotaManager) {
 
     this.storage = checkNotNull(storage);
-    this.cronScheduler = cronScheduler;
+    this.cronJobManager = cronJobManager;
     this.stateManager = checkNotNull(stateManager);
     this.taskIdGenerator = checkNotNull(taskIdGenerator);
     this.quotaManager = checkNotNull(quotaManager);
@@ -108,7 +112,20 @@ class SchedulerCoreImpl implements SchedulerCore {
         storage,
         Query.jobScoped(job.getKey()).active()).isEmpty();
 
-    return hasActiveTasks || cronScheduler.hasJob(job.getKey());
+    return hasActiveTasks || cronJobManager.hasJob(job.getKey());
+  }
+
+  private static boolean isCron(SanitizedConfiguration config) {
+    if (!config.getJobConfig().isSetCronSchedule()) {
+      return false;
+    } else if (StringUtils.isEmpty(config.getJobConfig().getCronSchedule())) {
+      // TODO(ksweeney): Remove this in 0.7.0 (AURORA-423).
+      LOG.warning("Got service config with empty string cron schedule. aurora-0.7.x "
+          + "will interpret this as cron job and cause an error.");
+      return false;
+    } else {
+      return true;
+    }
   }
 
   @Override
@@ -120,12 +137,19 @@ class SchedulerCoreImpl implements SchedulerCore {
       protected void execute(MutableStoreProvider storeProvider) throws ScheduleException {
         final IJobConfiguration job = sanitizedConfiguration.getJobConfig();
         if (hasActiveJob(job)) {
-          throw new ScheduleException("Job already exists: " + JobKeys.toPath(job));
+          throw new ScheduleException(
+              "Job already exists: " + JobKeys.canonicalString(job.getKey()));
         }
 
         validateTaskLimits(job.getTaskConfig(), job.getInstanceCount());
 
-        if (!cronScheduler.receiveJob(sanitizedConfiguration)) {
+        if (isCron(sanitizedConfiguration)) {
+          try {
+            cronJobManager.createJob(SanitizedCronJob.from(sanitizedConfiguration));
+          } catch (CronException e) {
+            throw new ScheduleException(e);
+          }
+        } else {
           LOG.info("Launching " + sanitizedConfiguration.getTaskConfigs().size() + " tasks.");
           stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
         }
@@ -216,7 +240,7 @@ class SchedulerCoreImpl implements SchedulerCore {
       // it.
       // TODO(maxim): Should be trivial to support killing multiple jobs instead.
       IJobKey jobKey = Iterables.getOnlyElement(JobKeys.from(query).get());
-      cronScheduler.deleteJob(jobKey);
+      cronJobManager.deleteJob(jobKey);
     }
 
     // Unless statuses were specifically supplied, only attempt to kill active tasks.

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/state/StateModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/state/StateModule.java b/src/main/java/org/apache/aurora/scheduler/state/StateModule.java
index 7d26082..9db2a1a 100644
--- a/src/main/java/org/apache/aurora/scheduler/state/StateModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/state/StateModule.java
@@ -49,17 +49,10 @@ public class StateModule extends AbstractModule {
     bind(LockManager.class).to(LockManagerImpl.class);
     bind(LockManagerImpl.class).in(Singleton.class);
 
-    bindCronJobManager(binder());
     bindMaintenanceController(binder());
   }
 
   @VisibleForTesting
-  static void bindCronJobManager(Binder binder) {
-    binder.bind(CronJobManager.class).in(Singleton.class);
-    PubsubEventModule.bindSubscriber(binder, CronJobManager.class);
-  }
-
-  @VisibleForTesting
   static void bindMaintenanceController(Binder binder) {
     binder.bind(MaintenanceController.class).to(MaintenanceControllerImpl.class);
     binder.bind(MaintenanceControllerImpl.class).in(Singleton.class);

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterface.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterface.java b/src/main/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterface.java
index 9bb5c25..f101143 100644
--- a/src/main/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterface.java
+++ b/src/main/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterface.java
@@ -94,11 +94,14 @@ import org.apache.aurora.scheduler.base.Tasks;
 import org.apache.aurora.scheduler.configuration.ConfigurationManager;
 import org.apache.aurora.scheduler.configuration.ConfigurationManager.TaskDescriptionException;
 import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
 import org.apache.aurora.scheduler.cron.CronPredictor;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
 import org.apache.aurora.scheduler.quota.QuotaInfo;
 import org.apache.aurora.scheduler.quota.QuotaManager;
 import org.apache.aurora.scheduler.quota.QuotaManager.QuotaException;
-import org.apache.aurora.scheduler.state.CronJobManager;
 import org.apache.aurora.scheduler.state.LockManager;
 import org.apache.aurora.scheduler.state.LockManager.LockException;
 import org.apache.aurora.scheduler.state.MaintenanceController;
@@ -242,7 +245,7 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
       schedulerCore.createJob(sanitized);
       response.setResponseCode(OK)
           .setMessage(String.format("%d new tasks pending for job %s",
-              sanitized.getJobConfig().getInstanceCount(), JobKeys.toPath(job)));
+              sanitized.getJobConfig().getInstanceCount(), JobKeys.canonicalString(job.getKey())));
     } catch (LockException e) {
       response.setResponseCode(LOCK_ERROR).setMessage(e.getMessage());
     } catch (TaskDescriptionException | ScheduleException e) {
@@ -275,11 +278,11 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
           ILockKey.build(LockKey.job(jobKey.newBuilder())),
           Optional.fromNullable(mutableLock).transform(ILock.FROM_BUILDER));
 
-      cronJobManager.updateJob(SanitizedConfiguration.fromUnsanitized(job));
+      cronJobManager.updateJob(SanitizedCronJob.fromUnsanitized(job));
       return response.setResponseCode(OK).setMessage("Replaced template for: " + jobKey);
     } catch (LockException e) {
       return response.setResponseCode(LOCK_ERROR).setMessage(e.getMessage());
-    } catch (TaskDescriptionException | ScheduleException e) {
+    } catch (CronException | TaskDescriptionException e) {
       return response.setResponseCode(INVALID_REQUEST).setMessage(e.getMessage());
     }
   }
@@ -314,21 +317,16 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
     try {
       sessionValidator.checkAuthenticated(session, ImmutableSet.of(jobKey.getRole()));
     } catch (AuthFailedException e) {
-      response.setResponseCode(AUTH_FAILED).setMessage(e.getMessage());
-      return response;
+      return response.setResponseCode(AUTH_FAILED).setMessage(e.getMessage());
     }
 
     try {
       cronJobManager.startJobNow(jobKey);
-      response.setResponseCode(OK).setMessage("Cron run started.");
-    } catch (ScheduleException e) {
-      response.setResponseCode(INVALID_REQUEST)
+      return response.setResponseCode(OK).setMessage("Cron run started.");
+    } catch (CronException e) {
+      return response.setResponseCode(INVALID_REQUEST)
           .setMessage("Failed to start cron job - " + e.getMessage());
-    } catch (TaskDescriptionException e) {
-      response.setResponseCode(ERROR).setMessage("Invalid task description: " + e.getMessage());
     }
-
-    return response;
   }
 
   // TODO(William Farner): Provide status information about cron jobs here.
@@ -386,13 +384,14 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
       @Override
       public JobSummary apply(IJobKey jobKey) {
         IJobConfiguration job = jobs.get(jobKey);
-        JobSummary smry = new JobSummary()
+        JobSummary summary = new JobSummary()
             .setJob(job.newBuilder())
             .setStats(Jobs.getJobStats(tasks.get(jobKey)).newBuilder());
 
         return Strings.isNullOrEmpty(job.getCronSchedule())
-            ? smry
-            : smry.setNextCronRunMs(cronPredictor.predictNextRun(job.getCronSchedule()).getTime());
+            ? summary
+            : summary.setNextCronRunMs(
+                cronPredictor.predictNextRun(CrontabEntry.parse(job.getCronSchedule())).getTime());
       }
     };
 
@@ -826,7 +825,8 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
               jobsByKey(jobStore, existingJob.getKey());
           switch (matches.size()) {
             case 0:
-              error = Optional.of("No jobs found for key " + JobKeys.toPath(existingJob));
+              error = Optional.of(
+                  "No jobs found for key " + JobKeys.canonicalString(existingJob.getKey()));
               break;
 
             case 1:
@@ -834,14 +834,16 @@ class SchedulerThriftInterface implements AuroraAdmin.Iface {
                   Iterables.getOnlyElement(matches.entries());
               IJobConfiguration storedJob = match.getValue();
               if (!storedJob.equals(existingJob)) {
-                error = Optional.of("CAS compare failed for " + JobKeys.toPath(storedJob));
+                error = Optional.of(
+                    "CAS compare failed for " + JobKeys.canonicalString(storedJob.getKey()));
               } else {
                 jobStore.saveAcceptedJob(match.getKey(), rewrittenJob);
               }
               break;
 
             default:
-              error = Optional.of("Multiple jobs found for key " + JobKeys.toPath(existingJob));
+              error = Optional.of("Multiple jobs found for key "
+                  + JobKeys.canonicalString(existingJob.getKey()));
           }
         }
         break;

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/python/apache/aurora/config/thrift.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/config/thrift.py b/src/main/python/apache/aurora/config/thrift.py
index 1798c40..0cd9246 100644
--- a/src/main/python/apache/aurora/config/thrift.py
+++ b/src/main/python/apache/aurora/config/thrift.py
@@ -250,9 +250,6 @@ def convert(job, metadata=frozenset(), ports=frozenset()):
   if unbound:
     raise InvalidConfig('Config contains unbound variables: %s' % ' '.join(map(str, unbound)))
 
-  cron_schedule = not_empty_or(job.cron_schedule(), '')
-  cron_policy = select_cron_policy(job.cron_policy(), job.cron_collision_policy())
-
   task.executorConfig = ExecutorConfig(
       name=AURORA_EXECUTOR_NAME,
       data=filter_aliased_fields(underlying).json_dumps())
@@ -260,7 +257,7 @@ def convert(job, metadata=frozenset(), ports=frozenset()):
   return JobConfiguration(
       key=key,
       owner=owner,
-      cronSchedule=cron_schedule,
-      cronCollisionPolicy=cron_policy,
+      cronSchedule=not_empty_or(job.cron_schedule(), None),
+      cronCollisionPolicy=select_cron_policy(job.cron_policy(), job.cron_collision_policy()),
       taskConfig=task,
       instanceCount=fully_interpolated(job.instances()))


[5/5] git commit: CronScheduler based on Quartz

Posted by ke...@apache.org.
CronScheduler based on Quartz

This introduces a new CronScheduler based on Quartz and removes the
NoopCronScheduler.

Testing Done:
./gradlew build

Bugs closed: AURORA-132, AURORA-349

Reviewed at https://reviews.apache.org/r/19767/


Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/3361dbed
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/3361dbed
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/3361dbed

Branch: refs/heads/master
Commit: 3361dbedfad8f2b1fcf05488a259f9c35546dc7c
Parents: 2759696
Author: Kevin Sweeney <ke...@apache.org>
Authored: Wed May 14 19:34:33 2014 -0700
Committer: Kevin Sweeney <ke...@apache.org>
Committed: Wed May 14 19:34:33 2014 -0700

----------------------------------------------------------------------
 build.gradle                                    |    1 +
 .../aurora/scheduler/MesosTaskFactory.java      |    2 +-
 .../aurora/scheduler/app/SchedulerMain.java     |   20 +-
 .../aurora/scheduler/async/TaskGroups.java      |    2 +-
 .../apache/aurora/scheduler/base/JobKeys.java   |   40 +-
 .../configuration/ConfigurationManager.java     |   12 +-
 .../aurora/scheduler/cron/CronJobManager.java   |   97 +
 .../aurora/scheduler/cron/CronPredictor.java    |    2 +-
 .../aurora/scheduler/cron/CronScheduler.java    |   40 +-
 .../aurora/scheduler/cron/CrontabEntryTest.java |  163 -
 .../aurora/scheduler/cron/SanitizedCronJob.java |  131 +
 .../scheduler/cron/noop/NoopCronModule.java     |   40 -
 .../scheduler/cron/noop/NoopCronPredictor.java  |   33 -
 .../scheduler/cron/noop/NoopCronScheduler.java  |   83 -
 .../scheduler/cron/quartz/AuroraCronJob.java    |  231 ++
 .../cron/quartz/AuroraCronJobFactory.java       |   49 +
 .../cron/quartz/CronJobManagerImpl.java         |  256 ++
 .../scheduler/cron/quartz/CronLifecycle.java    |  114 +
 .../scheduler/cron/quartz/CronModule.java       |  130 +
 .../cron/quartz/CronPredictorImpl.java          |   46 +
 .../cron/quartz/CronSchedulerImpl.java          |   71 +
 .../aurora/scheduler/cron/quartz/Quartz.java    |  124 +
 .../scheduler/cron/testing/AbstractCronIT.java  |  135 -
 .../org/apache/aurora/scheduler/http/Cron.java  |    3 +-
 .../aurora/scheduler/http/ServletModule.java    |    2 +-
 .../aurora/scheduler/http/StructDump.java       |   15 +-
 .../apache/aurora/scheduler/sla/SlaGroup.java   |    2 +-
 .../aurora/scheduler/state/CronJobManager.java  |  484 ---
 .../aurora/scheduler/state/LockManagerImpl.java |    2 +-
 .../scheduler/state/SchedulerCoreImpl.java      |   40 +-
 .../aurora/scheduler/state/StateModule.java     |    7 -
 .../thrift/SchedulerThriftInterface.java        |   40 +-
 src/main/python/apache/aurora/config/thrift.py  |    7 +-
 .../cron/testing/cron-schedule-predictions.json | 3332 ------------------
 .../aurora/scheduler/cron/CrontabEntryTest.java |  168 +
 .../scheduler/cron/ExpectedPrediction.java      |   57 +
 .../aurora/scheduler/cron/noop/NoopCronIT.java  |   60 -
 .../cron/quartz/AuroraCronJobTest.java          |  174 +
 .../aurora/scheduler/cron/quartz/CronIT.java    |  257 ++
 .../cron/quartz/CronJobManagerImplTest.java     |  221 ++
 .../cron/quartz/CronPredictorImplTest.java      |   89 +
 .../scheduler/cron/quartz/QuartzTestUtil.java   |   78 +
 .../state/BaseSchedulerCoreImplTest.java        |  366 +-
 .../scheduler/state/CronJobManagerTest.java     |  490 ---
 .../scheduler/state/LockManagerImplTest.java    |    2 +-
 .../thrift/SchedulerThriftInterfaceTest.java    |   17 +-
 .../aurora/scheduler/thrift/ThriftIT.java       |    4 +-
 .../scheduler/cron/expected-predictions.json    | 3332 ++++++++++++++++++
 48 files changed, 5831 insertions(+), 5240 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/build.gradle
----------------------------------------------------------------------
diff --git a/build.gradle b/build.gradle
index f2c729e..6f43fea 100644
--- a/build.gradle
+++ b/build.gradle
@@ -164,6 +164,7 @@ dependencies {
   compile 'org.apache.mesos:mesos:0.18.0'
   compile thriftLib
   compile 'org.apache.zookeeper:zookeeper:3.3.4'
+  compile 'org.quartz-scheduler:quartz:2.2.1'
   compile "org.slf4j:slf4j-api:${slf4jRev}"
   compile "org.slf4j:slf4j-jdk14:${slf4jRev}"
   compile 'com.twitter.common.logging:log4j:0.0.7'

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/MesosTaskFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/MesosTaskFactory.java b/src/main/java/org/apache/aurora/scheduler/MesosTaskFactory.java
index 86bbc29..bdd8c19 100644
--- a/src/main/java/org/apache/aurora/scheduler/MesosTaskFactory.java
+++ b/src/main/java/org/apache/aurora/scheduler/MesosTaskFactory.java
@@ -135,7 +135,7 @@ public interface MesosTaskFactory {
       }
       TaskInfo.Builder taskBuilder =
           TaskInfo.newBuilder()
-              .setName(JobKeys.toPath(Tasks.ASSIGNED_TO_JOB_KEY.apply(task)))
+              .setName(JobKeys.canonicalString(Tasks.ASSIGNED_TO_JOB_KEY.apply(task)))
               .setTaskId(TaskID.newBuilder().setValue(task.getTaskId()))
               .setSlaveId(slaveId)
               .addAllResources(resources)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java b/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java
index bf3d7a3..da6f5e5 100644
--- a/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java
+++ b/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java
@@ -59,9 +59,7 @@ import org.apache.aurora.scheduler.DriverFactory;
 import org.apache.aurora.scheduler.DriverFactory.DriverFactoryImpl;
 import org.apache.aurora.scheduler.MesosTaskFactory.ExecutorConfig;
 import org.apache.aurora.scheduler.SchedulerLifecycle;
-import org.apache.aurora.scheduler.cron.CronPredictor;
-import org.apache.aurora.scheduler.cron.CronScheduler;
-import org.apache.aurora.scheduler.cron.noop.NoopCronModule;
+import org.apache.aurora.scheduler.cron.quartz.CronModule;
 import org.apache.aurora.scheduler.local.IsolatedSchedulerModule;
 import org.apache.aurora.scheduler.log.mesos.MesosLogStreamModule;
 import org.apache.aurora.scheduler.storage.backup.BackupModule;
@@ -115,17 +113,7 @@ public class SchedulerMain extends AbstractApplication {
       .add(CapabilityValidator.class)
       .build();
 
-  @CmdLine(name = "cron_module",
-      help = "A Guice module to provide cron bindings. NOTE: The default is a no-op.")
-  private static final Arg<? extends Class<? extends Module>> CRON_MODULE =
-      Arg.create(NoopCronModule.class);
-
-  private static final Iterable<Class<?>> CRON_MODULE_CLASSES = ImmutableList.<Class<?>>builder()
-      .add(CronPredictor.class)
-      .add(CronScheduler.class)
-      .build();
-
-  // TODO(Suman Karumuri): Pass in AUTH and CRON modules as extra modules
+  // TODO(Suman Karumuri): Pass in AUTH as extra module
   @CmdLine(name = "extra_modules",
       help = "A list of modules that provide additional functionality.")
   private static final Arg<List<Class<? extends Module>>> EXTRA_MODULES =
@@ -151,8 +139,7 @@ public class SchedulerMain extends AbstractApplication {
 
   private static Iterable<? extends Module> getExtraModules() {
     Builder<Module> modules = ImmutableList.builder();
-    modules.add(Modules.wrapInPrivateModule(AUTH_MODULE.get(), AUTH_MODULE_CLASSES))
-        .add(Modules.wrapInPrivateModule(CRON_MODULE.get(), CRON_MODULE_CLASSES));
+    modules.add(Modules.wrapInPrivateModule(AUTH_MODULE.get(), AUTH_MODULE_CLASSES));
 
     for (Class<? extends Module> moduleClass : EXTRA_MODULES.get()) {
       modules.add(Modules.getModule(moduleClass));
@@ -173,6 +160,7 @@ public class SchedulerMain extends AbstractApplication {
         .addAll(getExtraModules())
         .add(new LogStorageModule())
         .add(new MemStorageModule(Bindings.annotatedKeyFactory(LogStorage.WriteBehind.class)))
+        .add(new CronModule())
         .add(new ThriftModule())
         .add(new ThriftAuthModule())
         .build();

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/async/TaskGroups.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/async/TaskGroups.java b/src/main/java/org/apache/aurora/scheduler/async/TaskGroups.java
index 38e19aa..54748b2 100644
--- a/src/main/java/org/apache/aurora/scheduler/async/TaskGroups.java
+++ b/src/main/java/org/apache/aurora/scheduler/async/TaskGroups.java
@@ -244,7 +244,7 @@ public class TaskGroups implements EventSubscriber {
 
     @Override
     public String toString() {
-      return JobKeys.toPath(Tasks.INFO_TO_JOB_KEY.apply(canonicalTask));
+      return JobKeys.canonicalString(Tasks.INFO_TO_JOB_KEY.apply(canonicalTask));
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/base/JobKeys.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/JobKeys.java b/src/main/java/org/apache/aurora/scheduler/base/JobKeys.java
index db1bec4..c81ac62 100644
--- a/src/main/java/org/apache/aurora/scheduler/base/JobKeys.java
+++ b/src/main/java/org/apache/aurora/scheduler/base/JobKeys.java
@@ -15,17 +15,20 @@
  */
 package org.apache.aurora.scheduler.base;
 
+import java.util.List;
 import java.util.Set;
 import javax.annotation.Nullable;
 
 import com.google.common.base.Function;
 import com.google.common.base.Functions;
+import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
-import com.google.common.base.Strings;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableSet;
 
 import org.apache.aurora.gen.JobKey;
 import org.apache.aurora.gen.TaskQuery;
+import org.apache.aurora.scheduler.configuration.ConfigurationManager;
 import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
 import org.apache.aurora.scheduler.storage.entities.IJobKey;
 import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
@@ -83,9 +86,9 @@ public final class JobKeys {
    */
   public static boolean isValid(@Nullable IJobKey jobKey) {
     return jobKey != null
-        && !Strings.isNullOrEmpty(jobKey.getRole())
-        && !Strings.isNullOrEmpty(jobKey.getEnvironment())
-        && !Strings.isNullOrEmpty(jobKey.getName());
+        && ConfigurationManager.isGoodIdentifier(jobKey.getRole())
+        && ConfigurationManager.isGoodIdentifier(jobKey.getEnvironment())
+        && ConfigurationManager.isGoodIdentifier(jobKey.getName());
   }
 
   /**
@@ -132,25 +135,32 @@ public final class JobKeys {
   }
 
   /**
-   * Create a "/"-delimited String representation of a job key, suitable for logging but not
-   * necessarily suitable for use as a unique identifier.
+   * Create a "/"-delimited representation of job key usable as a unique identifier in this cluster.
    *
+   * It is guaranteed that {@code k.equals(JobKeys.parse(JobKeys.canonicalString(k))}.
+   *
+   * @see #parse(String)
    * @param jobKey Key to represent.
-   * @return "/"-delimited representation of the key.
+   * @return Canonical "/"-delimited representation of the key.
    */
-  public static String toPath(IJobKey jobKey) {
-    return jobKey.getRole() + "/" + jobKey.getEnvironment() + "/" + jobKey.getName();
+  public static String canonicalString(IJobKey jobKey) {
+    return Joiner.on("/").join(jobKey.getRole(), jobKey.getEnvironment(), jobKey.getName());
   }
 
   /**
-   * Create a "/"-delimited String representation of job key, suitable for logging but not
-   * necessarily suitable for use as a unique identifier.
+   * Create a job key from a "role/environment/name" representation.
+   *
+   * It is guaranteed that {@code k.equals(JobKeys.parse(JobKeys.canonicalString(k))}.
    *
-   * @param job Job to represent.
-   * @return "/"-delimited representation of the job's key.
+   * @see #canonicalString(IJobKey)
+   * @param string Input to parse.
+   * @return Parsed representation.
+   * @throws IllegalArgumentException when the string fails to parse.
    */
-  public static String toPath(IJobConfiguration job) {
-    return toPath(job.getKey());
+  public static IJobKey parse(String string) throws IllegalArgumentException {
+    List<String> components = Splitter.on("/").splitToList(string);
+    checkArgument(components.size() == 3);
+    return from(components.get(0), components.get(1), components.get(2));
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
index 82034e0..e5ad461 100644
--- a/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
@@ -164,9 +164,15 @@ public final class ConfigurationManager {
     // Utility class.
   }
 
-  @VisibleForTesting
-  static boolean isGoodIdentifier(String identifier) {
-    return GOOD_IDENTIFIER.matcher(identifier).matches()
+  /**
+   * Verifies that an identifier is an acceptable name component.
+   *
+   * @param identifier Identifier to check.
+   * @return false if the identifier is null or invalid.
+   */
+  public static boolean isGoodIdentifier(@Nullable String identifier) {
+    return identifier != null
+        && GOOD_IDENTIFIER.matcher(identifier).matches()
         && (identifier.length() <= MAX_IDENTIFIER_LENGTH);
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/CronJobManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronJobManager.java b/src/main/java/org/apache/aurora/scheduler/cron/CronJobManager.java
new file mode 100644
index 0000000..7c8d5ec
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronJobManager.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron;
+
+import java.util.Map;
+
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+
+/**
+ * Manages the persistence and scheduling of jobs that should be run periodically on a cron
+ * schedule.
+ */
+public interface CronJobManager {
+  /**
+   * Triggers execution of a job.
+   *
+   * @param jobKey Key of the job to start.
+   * @throws CronException If the job could not be started with the cron system.
+   */
+  void startJobNow(IJobKey jobKey) throws CronException;
+
+  /**
+   * Persist a new cron job to storage and schedule it for future execution.
+   *
+   * @param config Cron job configuration to update to.
+   * @throws CronException If a job with the same key does not exist or the job could not be
+   * scheduled.
+   */
+  void updateJob(SanitizedCronJob config) throws CronException;
+
+  /**
+   * Persist a cron job to storage and schedule it for future execution.
+   *
+   * @param config New cron job configuration.
+   * @throws CronException If a job with the same key exists or the job could not be scheduled.
+   */
+  void createJob(SanitizedCronJob config) throws CronException;
+
+  /**
+   * Get all cron jobs.
+   *
+   * TODO(ksweeney): Consider deprecating this and letting caller query storage directly.
+   *
+   * @return An immutable snapshot of cron jobs at some instant.
+   */
+  Iterable<IJobConfiguration> getJobs();
+
+  /**
+   * Test whether a job exists.
+   *
+   * TODO(ksweeney): Consider deprecating this and letting caller query storage directly.
+   *
+   * @param jobKey Key of the job to check.
+   * @return false when a job does not exist in storage.
+   */
+  boolean hasJob(IJobKey jobKey);
+
+  /**
+   * Remove a job and deschedule it.
+   *
+   * @param jobKey Key of the job to delete.
+   * @return true if a job was removed.
+   */
+  boolean deleteJob(IJobKey jobKey);
+
+  /**
+   * A list of the currently scheduled jobs and when they will run according to the underlying
+   * execution engine.
+   *
+   * @return A map from job to the cron schedule in use for that job.
+   */
+  Map<IJobKey, CrontabEntry> getScheduledJobs();
+
+  /**
+   * The unique ID of this cron job manager, used as a prefix in the JobStore.
+   *
+   * TODO(ksweeney): Consider removing this from storage entirely since the JobManager abstraction
+   * is gone.
+   *
+   * @return The unique ID of the manager.
+   */
+  String getManagerKey();
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java b/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
index df0c378..0ce60f8 100644
--- a/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
@@ -27,5 +27,5 @@ public interface CronPredictor {
    * @param schedule Cron schedule to predict the next time for.
    * @return A prediction for the next time a cron will run.
    */
-  Date predictNextRun(String schedule);
+  Date predictNextRun(CrontabEntry schedule);
 }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java b/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
index 56e9950..f38dea5 100644
--- a/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
@@ -15,49 +15,19 @@
  */
 package org.apache.aurora.scheduler.cron;
 
-import javax.annotation.Nullable;
-
 import com.google.common.base.Optional;
-import com.google.common.util.concurrent.Service;
+
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
 
 /**
  * An execution manager that executes work on a cron schedule.
  */
-public interface CronScheduler extends Service {
-  /**
-   * Schedules a task on a cron schedule.
-   *
-   * @param schedule Cron-style schedule.
-   * @param task Work to run when on the cron schedule.
-   * @return A unique ID to identify the scheduled cron task.
-   * @throws CronException when there was a failure to schedule, for example if {@code schedule}
-   *         is not a valid input.
-   * @throws IllegalStateException If the cron scheduler is not currently running.
-   */
-  String schedule(String schedule, Runnable task) throws CronException, IllegalStateException;
-
-  /**
-   * Removes a scheduled cron item.
-   *
-   * @param key Key previously returned from {@link #schedule(String, Runnable)}.
-   * @throws IllegalStateException If the cron scheduler is not currently running.
-   */
-  void deschedule(String key) throws IllegalStateException;
-
+public interface CronScheduler {
   /**
    * Gets the cron schedule associated with a scheduling key.
    *
-   * @param key Key previously returned from {@link #schedule(String, Runnable)}.
+   * @param key Key previously returned from {@link #schedule(CrontabEntry, Runnable)}.
    * @return The task's cron schedule, if a matching task was found.
-   * @throws IllegalStateException If the cron scheduler is not currently running.
-   */
-  Optional<String> getSchedule(String key) throws IllegalStateException;
-
-  /**
-   * Checks to see if the scheduler would be accepted by the underlying scheduler.
-   *
-   * @param schedule Cron scheduler to validate.
-   * @return {@code true} if the schedule is valid.
    */
-  boolean isValidSchedule(@Nullable String schedule);
+  Optional<CrontabEntry> getSchedule(IJobKey key);
 }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java b/src/main/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
deleted file mode 100644
index 2bb848a..0000000
--- a/src/main/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * Copyright 2014 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron;
-
-import java.util.List;
-import java.util.Set;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-public class CrontabEntryTest {
-  @Test
-  public void testHashCodeAndEquals() {
-
-    List<CrontabEntry> entries = ImmutableList.of(
-        CrontabEntry.parse("* * * * *"),
-        CrontabEntry.parse("0-59 * * * *"),
-        CrontabEntry.parse("0-57,58,59 * * * *"),
-        CrontabEntry.parse("* 23,1,2,4,0-22 * * *"),
-        CrontabEntry.parse("1-50,0,51-59 * * * sun-sat"));
-
-    for (CrontabEntry lhs : entries) {
-      for (CrontabEntry rhs : entries) {
-        assertEquals(lhs, rhs);
-      }
-    }
-
-    Set<CrontabEntry> equivalentEntries = Sets.newHashSet(entries);
-    assertTrue(equivalentEntries.size() == 1);
-  }
-
-  @Test
-  public void testEqualsCoverage() {
-    assertNotEquals(CrontabEntry.parse("* * * * *"), new Object());
-
-    assertNotEquals(CrontabEntry.parse("* * * * *"), CrontabEntry.parse("1 * * * *"));
-    assertEquals(CrontabEntry.parse("1,2,3 * * * *"), CrontabEntry.parse("1-3 * * * *"));
-
-    assertNotEquals(CrontabEntry.parse("* 0-22 * * *"), CrontabEntry.parse("* * * * *"));
-    assertEquals(CrontabEntry.parse("* 0-23 * * *"), CrontabEntry.parse("* * * * *"));
-
-    assertNotEquals(CrontabEntry.parse("1 1 1-30 * *"), CrontabEntry.parse("1 1 * * *"));
-    assertEquals(CrontabEntry.parse("1 1 1-31 * *"), CrontabEntry.parse("1 1 * * *"));
-
-    assertNotEquals(CrontabEntry.parse("1 1 * JAN,FEB-NOV *"), CrontabEntry.parse("1 1 * * *"));
-    assertEquals(CrontabEntry.parse("1 1 * JAN,FEB-DEC *"), CrontabEntry.parse("1 1 * * *"));
-
-    assertNotEquals(CrontabEntry.parse("* * * * SUN"), CrontabEntry.parse("* * * * SAT"));
-    assertEquals(CrontabEntry.parse("* * * * 0"), CrontabEntry.parse("* * * * SUN"));
-  }
-
-  @Test
-  public void testSkip() {
-    assertEquals(CrontabEntry.parse("*/15 * * * *"), CrontabEntry.parse("0,15,30,45 * * * *"));
-    assertEquals(
-        CrontabEntry.parse("* */2 * * *"),
-        CrontabEntry.parse("0-59 0,2,4,6,8,10,12-23/2  * * *"));
-  }
-
-  @Test
-  public void testToString() {
-    assertEquals("0-58 * * * *", CrontabEntry.parse("0,1-57,58 * * * *").toString());
-    assertEquals("* * * * *", CrontabEntry.parse("* * * * *").toString());
-  }
-
-  @Test
-  public void testWildcards() {
-    CrontabEntry wildcardMinuteEntry = CrontabEntry.parse("* 1 1 1 *");
-    assertEquals("*", wildcardMinuteEntry.getMinuteAsString());
-    assertTrue(wildcardMinuteEntry.hasWildcardMinute());
-    assertFalse(wildcardMinuteEntry.hasWildcardHour());
-    assertFalse(wildcardMinuteEntry.hasWildcardDayOfMonth());
-    assertFalse(wildcardMinuteEntry.hasWildcardMonth());
-    assertTrue(wildcardMinuteEntry.hasWildcardDayOfWeek());
-
-    CrontabEntry wildcardHourEntry = CrontabEntry.parse("1 * 1 1 *");
-    assertEquals("*", wildcardHourEntry.getHourAsString());
-    assertFalse(wildcardHourEntry.hasWildcardMinute());
-    assertTrue(wildcardHourEntry.hasWildcardHour());
-    assertFalse(wildcardHourEntry.hasWildcardDayOfMonth());
-    assertFalse(wildcardHourEntry.hasWildcardMonth());
-    assertTrue(wildcardHourEntry.hasWildcardDayOfWeek());
-
-    CrontabEntry wildcardDayOfMonth = CrontabEntry.parse("1 1 * 1 *");
-    assertEquals("*", wildcardDayOfMonth.getDayOfMonthAsString());
-    assertFalse(wildcardDayOfMonth.hasWildcardMinute());
-    assertFalse(wildcardDayOfMonth.hasWildcardHour());
-    assertTrue(wildcardDayOfMonth.hasWildcardDayOfMonth());
-    assertFalse(wildcardDayOfMonth.hasWildcardMonth());
-    assertTrue(wildcardDayOfMonth.hasWildcardDayOfWeek());
-
-    CrontabEntry wildcardMonth = CrontabEntry.parse("1 1 1 * *");
-    assertEquals("*", wildcardMonth.getMonthAsString());
-    assertFalse(wildcardMonth.hasWildcardMinute());
-    assertFalse(wildcardMonth.hasWildcardHour());
-    assertFalse(wildcardMonth.hasWildcardDayOfMonth());
-    assertTrue(wildcardMonth.hasWildcardMonth());
-    assertTrue(wildcardMonth.hasWildcardDayOfWeek());
-
-    CrontabEntry wildcardDayOfWeek = CrontabEntry.parse("1 1 1 1 *");
-    assertEquals("*", wildcardDayOfWeek.getDayOfWeekAsString());
-    assertFalse(wildcardDayOfWeek.hasWildcardMinute());
-    assertFalse(wildcardDayOfWeek.hasWildcardHour());
-    assertFalse(wildcardDayOfWeek.hasWildcardDayOfMonth());
-    assertFalse(wildcardDayOfWeek.hasWildcardMonth());
-    assertTrue(wildcardDayOfWeek.hasWildcardDayOfWeek());
-  }
-
-  @Test
-  public void testEqualsIsCanonical() {
-    String rawEntry = "* * */3 * *";
-    CrontabEntry input = CrontabEntry.parse(rawEntry);
-    assertNotEquals(
-        rawEntry + " is not the canonical form of " + input,
-        rawEntry,
-        input.toString());
-    assertEquals(
-        "The form returned by toString is canonical",
-        input.toString(),
-        CrontabEntry.parse(input.toString()).toString());
-  }
-
-  @Test
-  public void testBadEntries() {
-    List<String> badPatterns = ImmutableList.of(
-        "* * * * MON-SUN",
-        "* * **",
-        "0-59 0-59 * * *",
-        "1/1 * * * *",
-        "5 5 * MAR-JAN *",
-        "*/0 * * * *",
-        "0-59/0 * * * *",
-        "0-59/60 * * * *",
-        "* * * *, *",
-        "* * 1 * 1"
-    );
-
-    for (String pattern : badPatterns) {
-      assertNull(CrontabEntry.tryParse(pattern).orNull());
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/SanitizedCronJob.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/SanitizedCronJob.java b/src/main/java/org/apache/aurora/scheduler/cron/SanitizedCronJob.java
new file mode 100644
index 0000000..0082932
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/SanitizedCronJob.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Optional;
+
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.configuration.ConfigurationManager;
+import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.commons.lang.StringUtils;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Used by functions that expect field validation before being called.
+ */
+public final class SanitizedCronJob {
+  private final SanitizedConfiguration config;
+  private final CrontabEntry crontabEntry;
+
+  private SanitizedCronJob(IJobConfiguration unsanitized)
+      throws CronException, ConfigurationManager.TaskDescriptionException {
+
+    this(SanitizedConfiguration.fromUnsanitized(unsanitized));
+  }
+
+  private SanitizedCronJob(SanitizedConfiguration config) throws CronException {
+    final IJobConfiguration job = config.getJobConfig();
+    if (!hasCronSchedule(job)) {
+      throw new CronException(String.format(
+          "Not a valid cron job, %s has no cron schedule", JobKeys.canonicalString(job.getKey())));
+    }
+
+    Optional<CrontabEntry> entry = CrontabEntry.tryParse(job.getCronSchedule());
+    if (!entry.isPresent()) {
+      throw new CronException("Invalid cron schedule: " + job.getCronSchedule());
+    }
+
+    this.config = config;
+    this.crontabEntry = entry.get();
+  }
+
+  /**
+   * Get the default cron collision policy.
+   *
+   * @param policy A (possibly null) policy.
+   * @return The given policy or a default if the policy was null.
+   */
+  public static CronCollisionPolicy orDefault(@Nullable CronCollisionPolicy policy) {
+    return Optional.fromNullable(policy).or(CronCollisionPolicy.KILL_EXISTING);
+  }
+
+  /**
+   * Create a SanitizedCronJob from a SanitizedConfiguration. SanitizedCronJob performs additional
+   * validation to ensure that the provided job contains all properties needed to run it on a
+   * cron schedule.
+   *
+   * @param config Config to validate.
+   * @return Config wrapped in defaults.
+   * @throws CronException If a cron-specific validation error occured.
+   */
+  public static SanitizedCronJob from(SanitizedConfiguration config)
+      throws CronException {
+
+    return new SanitizedCronJob(config);
+  }
+
+  /**
+   * Create a cron job from an unsanitized input job. Suitable for RPC input validation.
+   *
+   * @param unsanitized Unsanitized input job.
+   * @return A sanitized job if all validation succeeds.
+   * @throws CronException If validation fails with a cron-specific error.
+   * @throws ConfigurationManager.TaskDescriptionException If validation fails with a non
+   * cron-specific error.
+   */
+  public static SanitizedCronJob fromUnsanitized(IJobConfiguration unsanitized)
+      throws CronException, ConfigurationManager.TaskDescriptionException {
+
+    return new SanitizedCronJob(unsanitized);
+  }
+
+  /**
+   * Get this job's cron collision policy.
+   *
+   * @return This job's cron collision policy.
+   */
+  public CronCollisionPolicy getCronCollisionPolicy() {
+    return orDefault(config.getJobConfig().getCronCollisionPolicy());
+  }
+
+  private static boolean hasCronSchedule(IJobConfiguration job) {
+    checkNotNull(job);
+    return !StringUtils.isEmpty(job.getCronSchedule());
+  }
+
+  /**
+   * Returns the cron schedule associated with this job.
+   *
+   * @return The cron schedule associated with this job.
+   */
+  public CrontabEntry getCrontabEntry() {
+    return crontabEntry;
+  }
+
+  /**
+   * Returns the sanitized job configuration associated with the cron job.
+   *
+   * @return This cron job's sanitized job configuration.
+   */
+  public SanitizedConfiguration getSanitizedConfig() {
+    return config;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
deleted file mode 100644
index e0935f5..0000000
--- a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron.noop;
-
-import javax.inject.Singleton;
-
-import com.google.inject.AbstractModule;
-
-import org.apache.aurora.scheduler.cron.CronPredictor;
-import org.apache.aurora.scheduler.cron.CronScheduler;
-
-/**
- * A Module to wire up a cron scheduler that does not actually schedule cron jobs.
- *
- * This class exists as a short term hack to get around a license compatibility issue - Real
- * Implementation (TM) coming soon.
- */
-public class NoopCronModule extends AbstractModule {
-  @Override
-  protected void configure() {
-    bind(CronScheduler.class).to(NoopCronScheduler.class);
-    bind(NoopCronScheduler.class).in(Singleton.class);
-
-    bind(CronPredictor.class).to(NoopCronPredictor.class);
-    bind(NoopCronPredictor.class).in(Singleton.class);
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
deleted file mode 100644
index 7b25152..0000000
--- a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron.noop;
-
-import java.util.Date;
-
-import org.apache.aurora.scheduler.cron.CronPredictor;
-
-/**
- * A cron predictor that always suggests that the next run is Unix epoch time.
- *
- * This class exists as a short term hack to get around a license compatibility issue - Real
- * Implementation (TM) coming soon.
- */
-class NoopCronPredictor implements CronPredictor {
-  @Override
-  public Date predictNextRun(String schedule) {
-    return new Date(0);
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
deleted file mode 100644
index a31551c..0000000
--- a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron.noop;
-
-import java.util.Collections;
-import java.util.Set;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.AbstractIdleService;
-
-import org.apache.aurora.scheduler.cron.CronScheduler;
-
-/**
- * A cron scheduler that accepts cron jobs but never runs them. Useful if you want to hook up an
- * external triggering mechanism (e.g. a system cron job that calls the startCronJob RPC manually
- * on an interval).
- *
- * This class exists as a short term hack to get around a license compatibility issue - Real
- * Implementation (TM) coming soon.
- */
-class NoopCronScheduler extends AbstractIdleService implements CronScheduler {
-  private static final Logger LOG = Logger.getLogger(NoopCronScheduler.class.getName());
-
-  // Keep a list of schedules we've seen.
-  private final Set<String> schedules = Collections.synchronizedSet(Sets.<String>newHashSet());
-
-  @Override
-  public void startUp() throws Exception {
-    LOG.warning("NO-OP cron scheduler is in use. Cron jobs submitted will not be triggered!");
-  }
-
-  @Override
-  public void shutDown() {
-    // No-op.
-  }
-
-  @Override
-  public String schedule(String schedule, Runnable task) {
-    schedules.add(schedule);
-
-    LOG.warning(String.format(
-        "NO-OP cron scheduler is in use! %s with schedule %s WILL NOT be automatically triggered!",
-        task,
-        schedule));
-
-    return schedule;
-  }
-
-  @Override
-  public void deschedule(String key) throws IllegalStateException {
-    schedules.remove(key);
-  }
-
-  @Override
-  public Optional<String> getSchedule(String key) throws IllegalStateException {
-    return schedules.contains(key)
-        ? Optional.of(key)
-        : Optional.<String>absent();
-  }
-
-  @Override
-  public boolean isValidSchedule(@Nullable String schedule) {
-    // Accept everything.
-    return schedule != null;
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJob.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJob.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJob.java
new file mode 100644
index 0000000..fc02264
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJob.java
@@ -0,0 +1,231 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.inject.Inject;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+
+import com.twitter.common.base.Supplier;
+import com.twitter.common.stats.Stats;
+import com.twitter.common.util.BackoffHelper;
+
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.gen.ScheduleStatus;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.base.Query;
+import org.apache.aurora.scheduler.base.Tasks;
+import org.apache.aurora.scheduler.configuration.ConfigurationManager;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.state.StateManager;
+import org.apache.aurora.scheduler.storage.Storage;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import static org.apache.aurora.gen.ScheduleStatus.KILLING;
+
+/**
+ * Encapsulates the logic behind a single trigger of a single job key. Multiple executions may run
+ * concurrently but only a single instance will be active at a time per job key.
+ *
+ * <p>
+ * Executions may block for long periods of time when waiting for a kill to complete. The Quartz
+ * scheduler should therefore be configured with a large number of threads.
+ */
+@DisallowConcurrentExecution
+class AuroraCronJob implements Job {
+  private static final Logger LOG = Logger.getLogger(AuroraCronJob.class.getName());
+
+  private static final AtomicLong CRON_JOB_TRIGGERS = Stats.exportLong("cron_job_triggers");
+  private static final AtomicLong CRON_JOB_MISFIRES = Stats.exportLong("cron_job_misfires");
+  private static final AtomicLong CRON_JOB_PARSE_FAILURES =
+      Stats.exportLong("cron_job_parse_failures");
+  private static final AtomicLong CRON_JOB_COLLISIONS = Stats.exportLong("cron_job_collisions");
+
+  @VisibleForTesting
+  static final Optional<String> KILL_AUDIT_MESSAGE = Optional.of("Killed by cronScheduler");
+
+  private final Storage storage;
+  private final StateManager stateManager;
+  private final CronJobManager cronJobManager;
+  private final BackoffHelper delayedStartBackoff;
+
+  @Inject
+  AuroraCronJob(
+      Config config,
+      Storage storage,
+      StateManager stateManager,
+      CronJobManager cronJobManager) {
+
+    this.storage = checkNotNull(storage);
+    this.stateManager = checkNotNull(stateManager);
+    this.cronJobManager = checkNotNull(cronJobManager);
+    this.delayedStartBackoff = checkNotNull(config.getDelayedStartBackoff());
+  }
+
+  private static final class DeferredLaunch {
+    private final Map<Integer, ITaskConfig> pendingTasks;
+    private final Set<String> activeTaskIds;
+
+    private DeferredLaunch(Map<Integer, ITaskConfig> pendingTasks, Set<String> activeTaskIds) {
+      this.pendingTasks = pendingTasks;
+      this.activeTaskIds = activeTaskIds;
+    }
+  }
+
+  @Override
+  public void execute(JobExecutionContext context) throws JobExecutionException {
+    // We assume quartz prevents concurrent runs of this job for a given job key. This allows us
+    // to avoid races where we might kill another run's tasks.
+    checkState(context.getJobDetail().isConcurrentExectionDisallowed());
+
+    doExecute(Quartz.auroraJobKey(context.getJobDetail().getKey()));
+  }
+
+  @VisibleForTesting
+  void doExecute(final IJobKey key) throws JobExecutionException {
+    final String path = JobKeys.canonicalString(key);
+
+    final Optional<DeferredLaunch> deferredLaunch = storage.write(
+        new Storage.MutateWork.Quiet<Optional<DeferredLaunch>>() {
+          @Override
+          public Optional<DeferredLaunch> apply(Storage.MutableStoreProvider storeProvider) {
+            Optional<IJobConfiguration> config =
+                storeProvider.getJobStore().fetchJob(cronJobManager.getManagerKey(), key);
+            if (!config.isPresent()) {
+              LOG.warning(String.format(
+                  "Cron was triggered for %s but no job with that key was found in storage.",
+                  path));
+              CRON_JOB_MISFIRES.incrementAndGet();
+              return Optional.absent();
+            }
+
+            SanitizedCronJob cronJob;
+            try {
+              cronJob = SanitizedCronJob.fromUnsanitized(config.get());
+            } catch (ConfigurationManager.TaskDescriptionException | CronException e) {
+              LOG.warning(String.format(
+                  "Invalid cron job for %s in storage - failed to parse with %s", key, e));
+              CRON_JOB_PARSE_FAILURES.incrementAndGet();
+              return Optional.absent();
+            }
+
+            CronCollisionPolicy collisionPolicy = cronJob.getCronCollisionPolicy();
+            LOG.info(String.format(
+                "Cron triggered for %s at %s with policy %s", path, new Date(), collisionPolicy));
+            CRON_JOB_TRIGGERS.incrementAndGet();
+
+            ImmutableMap<Integer, ITaskConfig> pendingTasks =
+                ImmutableMap.copyOf(cronJob.getSanitizedConfig().getTaskConfigs());
+
+            final Query.Builder activeQuery = Query.jobScoped(key).active();
+            Set<String> activeTasks =
+                Tasks.ids(storeProvider.getTaskStore().fetchTasks(activeQuery));
+
+            if (activeTasks.isEmpty()) {
+              stateManager.insertPendingTasks(pendingTasks);
+              return Optional.absent();
+            }
+
+            CRON_JOB_COLLISIONS.incrementAndGet();
+            switch (collisionPolicy) {
+              case KILL_EXISTING:
+                return Optional.of(new DeferredLaunch(pendingTasks, activeTasks));
+
+              case RUN_OVERLAP:
+                LOG.severe(String.format("Ignoring trigger for job %s with deprecated collision"
+                    + "policy RUN_OVERLAP due to unterminated active tasks.", path));
+                return Optional.absent();
+
+              case CANCEL_NEW:
+                return Optional.absent();
+
+              default:
+                LOG.severe("Unrecognized cron collision policy: " + collisionPolicy);
+                return Optional.absent();
+            }
+          }
+        }
+    );
+
+    if (!deferredLaunch.isPresent()) {
+      return;
+    }
+
+    for (String taskId : deferredLaunch.get().activeTaskIds) {
+      stateManager.changeState(
+          taskId,
+          Optional.<ScheduleStatus>absent(),
+          KILLING,
+          KILL_AUDIT_MESSAGE);
+    }
+    LOG.info(String.format("Waiting for job to terminate before launching cron job %s.", path));
+
+    final Query.Builder query = Query.taskScoped(deferredLaunch.get().activeTaskIds).active();
+    try {
+      // NOTE: We block the quartz execution thread here until we've successfully killed our
+      // ancestor. We mitigate this by using a cached thread pool for quartz.
+      delayedStartBackoff.doUntilSuccess(new Supplier<Boolean>() {
+        @Override
+        public Boolean get() {
+          if (Storage.Util.consistentFetchTasks(storage, query).isEmpty()) {
+            LOG.info("Initiating delayed launch of cron " + path);
+            stateManager.insertPendingTasks(deferredLaunch.get().pendingTasks);
+            return true;
+          } else {
+            LOG.info("Not yet safe to run cron " + path);
+            return false;
+          }
+        }
+      });
+    } catch (InterruptedException e) {
+      LOG.log(Level.WARNING, "Interrupted while trying to launch cron " + path, e);
+      Thread.currentThread().interrupt();
+      throw new JobExecutionException(e);
+    }
+  }
+
+  static class Config {
+    private final BackoffHelper delayedStartBackoff;
+
+    Config(BackoffHelper delayedStartBackoff) {
+      this.delayedStartBackoff = checkNotNull(delayedStartBackoff);
+    }
+
+    public BackoffHelper getDelayedStartBackoff() {
+      return delayedStartBackoff;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobFactory.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobFactory.java
new file mode 100644
index 0000000..c5268cb
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobFactory.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.quartz.Job;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.spi.JobFactory;
+import org.quartz.spi.TriggerFiredBundle;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+* Adapter that allows AuroraCronJobs to be constructed by Guice instead of directly by quartz.
+*/
+class AuroraCronJobFactory implements JobFactory {
+  private final Provider<AuroraCronJob> auroraCronJobProvider;
+
+  @Inject
+  AuroraCronJobFactory(Provider<AuroraCronJob> auroraCronJobProvider) {
+    this.auroraCronJobProvider = checkNotNull(auroraCronJobProvider);
+  }
+
+  @Override
+  public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
+    checkState(AuroraCronJob.class.equals(bundle.getJobDetail().getJobClass()),
+        "Quartz tried to run a type of job we don't know about: "
+            + bundle.getJobDetail().getJobClass());
+
+    return auroraCronJobProvider.get();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImpl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImpl.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImpl.java
new file mode 100644
index 0000000..8a5f569
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImpl.java
@@ -0,0 +1,256 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.storage.JobStore;
+import org.apache.aurora.scheduler.storage.Storage;
+import org.apache.aurora.scheduler.storage.Storage.MutateWork;
+import org.apache.aurora.scheduler.storage.Storage.Work;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.quartz.JobDetail;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.impl.matchers.GroupMatcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * NOTE: The source of truth for whether a cron job exists or not is always the JobStore. If state
+ * somehow becomes inconsistent (i.e. a job key is scheduled for execution but its underlying
+ * JobConfiguration does not exist in storage the execution of the job will log a warning and
+ * exit).
+ */
+class CronJobManagerImpl implements CronJobManager {
+  private static final Logger LOG = Logger.getLogger(CronJobManagerImpl.class.getName());
+
+  private final Storage storage;
+  private final Scheduler scheduler;
+  private final TimeZone timeZone;
+
+  @Inject
+  CronJobManagerImpl(Storage storage, Scheduler scheduler, TimeZone timeZone) {
+    this.storage = checkNotNull(storage);
+    this.scheduler = checkNotNull(scheduler);
+    this.timeZone = checkNotNull(timeZone);
+  }
+
+  @Override
+  public String getManagerKey() {
+    return "CRON";
+  }
+
+  @Override
+  public void startJobNow(final IJobKey jobKey) throws CronException {
+    checkNotNull(jobKey);
+
+    storage.weaklyConsistentRead(new Work<Void, CronException>() {
+      @Override
+      public Void apply(Storage.StoreProvider storeProvider) throws CronException {
+        checkCronExists(jobKey, storeProvider.getJobStore());
+        triggerJob(jobKey);
+        return null;
+      }
+    });
+  }
+
+  private void triggerJob(IJobKey jobKey) throws CronException {
+    try {
+      scheduler.triggerJob(Quartz.jobKey(jobKey));
+    } catch (SchedulerException e) {
+      throw new CronException(e);
+    }
+    LOG.info(String.format("Triggered cron job for %s.", JobKeys.canonicalString(jobKey)));
+  }
+
+  private static void checkNoRunOverlap(SanitizedCronJob cronJob) throws CronException {
+    // NOTE: We check at create and update instead of in SanitizedCronJob to allow existing jobs
+    // but reject new ones.
+    if (CronCollisionPolicy.RUN_OVERLAP.equals(cronJob.getCronCollisionPolicy())) {
+      throw new CronException(
+          "The RUN_OVERLAP collision policy has been removed (AURORA-38).");
+    }
+  }
+
+  @Override
+  public void updateJob(final SanitizedCronJob config) throws CronException {
+    checkNotNull(config);
+    checkNoRunOverlap(config);
+
+    final IJobKey jobKey = config.getSanitizedConfig().getJobConfig().getKey();
+    storage.write(new MutateWork.NoResult<CronException>() {
+      @Override
+      public void execute(Storage.MutableStoreProvider storeProvider) throws CronException {
+        checkCronExists(jobKey, storeProvider.getJobStore());
+
+        removeJob(jobKey, storeProvider.getJobStore());
+        descheduleJob(jobKey);
+        saveJob(config, storeProvider.getJobStore());
+        scheduleJob(config.getCrontabEntry(), jobKey);
+      }
+    });
+  }
+
+  @Override
+  public void createJob(final SanitizedCronJob cronJob) throws CronException {
+    checkNotNull(cronJob);
+    checkNoRunOverlap(cronJob);
+
+    final IJobKey jobKey = cronJob.getSanitizedConfig().getJobConfig().getKey();
+    storage.write(new MutateWork.NoResult<CronException>() {
+      @Override
+      protected void execute(Storage.MutableStoreProvider storeProvider) throws CronException {
+        checkNotExists(jobKey, storeProvider.getJobStore());
+
+        saveJob(cronJob, storeProvider.getJobStore());
+        scheduleJob(cronJob.getCrontabEntry(), jobKey);
+      }
+    });
+  }
+
+  private void checkNotExists(IJobKey jobKey, JobStore jobStore) throws CronException {
+    if (jobStore.fetchJob(getManagerKey(), jobKey).isPresent()) {
+      throw new CronException(
+          String.format("Job already exists for %s.", JobKeys.canonicalString(jobKey)));
+    }
+  }
+
+  private void checkCronExists(IJobKey jobKey, JobStore jobStore) throws CronException {
+    if (!jobStore.fetchJob(getManagerKey(), jobKey).isPresent()) {
+      throw new CronException(
+          String.format("No cron template found for %s.", JobKeys.canonicalString(jobKey)));
+    }
+  }
+
+  private void removeJob(IJobKey jobKey, JobStore.Mutable jobStore) {
+    jobStore.removeJob(jobKey);
+    LOG.info(
+        String.format("Deleted cron job %s from storage.", JobKeys.canonicalString(jobKey)));
+  }
+
+  private void saveJob(SanitizedCronJob cronJob, JobStore.Mutable jobStore) {
+    IJobConfiguration config = cronJob.getSanitizedConfig().getJobConfig();
+
+    jobStore.saveAcceptedJob(getManagerKey(), config);
+    LOG.info(String.format(
+        "Saved new cron job %s to storage.", JobKeys.canonicalString(config.getKey())));
+  }
+
+  // TODO(ksweeney): Consider exposing this in the interface and making caller responsible.
+  void scheduleJob(CrontabEntry crontabEntry, IJobKey jobKey) throws CronException {
+    try {
+      scheduler.scheduleJob(
+          Quartz.jobDetail(jobKey, AuroraCronJob.class),
+          Quartz.cronTrigger(crontabEntry, timeZone));
+    } catch (SchedulerException e) {
+      throw new CronException(e);
+    }
+    LOG.info(String.format(
+        "Scheduled job %s with schedule %s.", JobKeys.canonicalString(jobKey), crontabEntry));
+  }
+
+  @Override
+  public Iterable<IJobConfiguration> getJobs() {
+    // NOTE: no synchronization is needed here since we don't touch internal quartz state.
+    return storage.consistentRead(new Work.Quiet<Iterable<IJobConfiguration>>() {
+      @Override
+      public Iterable<IJobConfiguration> apply(Storage.StoreProvider storeProvider) {
+        return storeProvider.getJobStore().fetchJobs(getManagerKey());
+      }
+    });
+  }
+
+  @Override
+  public boolean hasJob(final IJobKey jobKey) {
+    checkNotNull(jobKey);
+
+    return storage.consistentRead(new Work.Quiet<Boolean>() {
+      @Override
+      public Boolean apply(Storage.StoreProvider storeProvider) {
+        return storeProvider.getJobStore().fetchJob(getManagerKey(), jobKey).isPresent();
+      }
+    });
+  }
+
+  @Override
+  public boolean deleteJob(final IJobKey jobKey) {
+    checkNotNull(jobKey);
+
+    return storage.write(new MutateWork.Quiet<Boolean>() {
+      @Override
+      public Boolean apply(Storage.MutableStoreProvider storeProvider) {
+        if (!hasJob(jobKey)) {
+          return false;
+        }
+
+        removeJob(jobKey, storeProvider.getJobStore());
+        descheduleJob(jobKey);
+        return true;
+      }
+    });
+  }
+
+  private void descheduleJob(IJobKey jobKey) {
+    String path = JobKeys.canonicalString(jobKey);
+    try {
+      // TODO(ksweeney): Consider interrupting the running job here.
+      // There's a race here where an old running job could fail to find the old config. That's
+      // fine given that the behavior of AuroraCronJob is to log an error and exit if it's unable
+      // to find a job for its key.
+      scheduler.deleteJob(Quartz.jobKey(jobKey));
+      LOG.info("Successfully descheduled " + path + ".");
+    } catch (SchedulerException e) {
+      LOG.log(Level.WARNING, "Error when attempting to deschedule " + path + ": " + e, e);
+    }
+  }
+
+  @Override
+  public Map<IJobKey, CrontabEntry> getScheduledJobs() {
+    // NOTE: no synchronization is needed here since this is just a dump of internal quartz state
+    // for debugging.
+    ImmutableMap.Builder<IJobKey, CrontabEntry> scheduledJobs = ImmutableMap.builder();
+    try {
+      for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.<JobKey>anyGroup())) {
+        Optional<JobDetail> jobDetail = Optional.fromNullable(scheduler.getJobDetail(jobKey));
+        if (jobDetail.isPresent()) {
+          scheduledJobs.put(
+              Quartz.auroraJobKey(jobKey), CrontabEntry.parse(jobDetail.get().getDescription()));
+        }
+      }
+    } catch (SchedulerException e) {
+      throw Throwables.propagate(e);
+    }
+    return scheduledJobs.build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronLifecycle.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronLifecycle.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronLifecycle.java
new file mode 100644
index 0000000..64fa068
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronLifecycle.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.common.util.concurrent.AbstractIdleService;
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.base.Command;
+import com.twitter.common.stats.Stats;
+
+import org.apache.aurora.scheduler.configuration.ConfigurationManager;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.events.PubsubEvent;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.quartz.Scheduler;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Manager for startup and teardown of Quartz scheduler.
+ */
+class CronLifecycle extends AbstractIdleService implements PubsubEvent.EventSubscriber {
+  private static final Logger LOG = Logger.getLogger(CronLifecycle.class.getName());
+
+  private static final AtomicInteger RUNNING_FLAG = Stats.exportInt("quartz_scheduler_running");
+  private static final AtomicInteger LOADED_FLAG = Stats.exportInt("cron_jobs_loaded");
+  private static final AtomicLong LAUNCH_FAILURES = Stats.exportLong("cron_job_launch_failures");
+
+  private final Scheduler scheduler;
+  private final ShutdownRegistry shutdownRegistry;
+  private final CronJobManagerImpl cronJobManager;
+
+  @Inject
+  CronLifecycle(
+      Scheduler scheduler,
+      ShutdownRegistry shutdownRegistry,
+      CronJobManagerImpl cronJobManager) {
+
+    this.scheduler = checkNotNull(scheduler);
+    this.shutdownRegistry = checkNotNull(shutdownRegistry);
+    this.cronJobManager = checkNotNull(cronJobManager);
+  }
+
+  /**
+   * Notifies the cronScheduler job manager that the scheduler is active, and job configurations
+   * are ready to load.
+   *
+   * @param schedulerActive Event.
+   */
+  @Subscribe
+  public void schedulerActive(PubsubEvent.SchedulerActive schedulerActive) {
+    startAsync();
+    shutdownRegistry.addAction(new Command() {
+      @Override
+      public void execute() {
+        CronLifecycle.this.stopAsync().awaitTerminated();
+      }
+    });
+    awaitRunning();
+  }
+
+  @Override
+  protected void startUp() throws Exception {
+    LOG.info("Starting Quartz cron scheduler" + scheduler.getSchedulerName() + ".");
+    scheduler.start();
+    RUNNING_FLAG.set(1);
+
+    // TODO(ksweeney): Refactor the interface - we really only need the job keys here.
+    for (IJobConfiguration job : cronJobManager.getJobs()) {
+      try {
+        SanitizedCronJob cronJob = SanitizedCronJob.fromUnsanitized(job);
+        cronJobManager.scheduleJob(
+            cronJob.getCrontabEntry(),
+            cronJob.getSanitizedConfig().getJobConfig().getKey());
+      } catch (CronException | ConfigurationManager.TaskDescriptionException e) {
+        logLaunchFailure(job, e);
+      }
+    }
+    LOADED_FLAG.set(1);
+  }
+
+  private void logLaunchFailure(IJobConfiguration job, Exception e) {
+    LAUNCH_FAILURES.incrementAndGet();
+    LOG.log(Level.SEVERE, "Scheduling failed for recovered job " + job, e);
+  }
+
+  @Override
+  protected void shutDown() throws Exception {
+    LOG.info("Shutting down Quartz cron scheduler.");
+    scheduler.shutdown();
+    RUNNING_FLAG.set(0);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronModule.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronModule.java
new file mode 100644
index 0000000..6934828
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronModule.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+import javax.inject.Singleton;
+
+import com.google.common.base.Throwables;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.twitter.common.args.Arg;
+import com.twitter.common.args.CmdLine;
+import com.twitter.common.quantity.Amount;
+import com.twitter.common.quantity.Time;
+import com.twitter.common.util.BackoffHelper;
+
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.CronPredictor;
+import org.apache.aurora.scheduler.cron.CronScheduler;
+import org.apache.aurora.scheduler.events.PubsubEventModule;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.impl.DirectSchedulerFactory;
+import org.quartz.simpl.RAMJobStore;
+import org.quartz.simpl.SimpleThreadPool;
+
+/**
+ * Provides a {@link CronJobManager} with a Quartz backend. While Quartz itself supports
+ * persistence, the scheduler exposed by this module does not persist any state - it simply
+ * creates tasks from a {@link org.apache.aurora.gen.JobConfiguration} template on a cron-style
+ * schedule.
+ */
+public class CronModule extends AbstractModule {
+  private static final Logger LOG = Logger.getLogger(CronModule.class.getName());
+
+  @CmdLine(name = "cron_scheduler_num_threads",
+      help = "Number of threads to use for the cron scheduler thread pool.")
+  private static final Arg<Integer> NUM_THREADS = Arg.create(100);
+
+  @CmdLine(name = "cron_timezone", help = "TimeZone to use for cron predictions.")
+  private static final Arg<String> CRON_TIMEZONE = Arg.create("GMT");
+
+  @CmdLine(name = "cron_start_initial_backoff", help =
+      "Initial backoff delay while waiting for a previous cron run to be killed.")
+  public static final Arg<Amount<Long, Time>> CRON_START_INITIAL_BACKOFF =
+      Arg.create(Amount.of(1L, Time.SECONDS));
+
+  @CmdLine(name = "cron_start_max_backoff", help =
+      "Max backoff delay while waiting for a previous cron run to be killed.")
+  public static final Arg<Amount<Long, Time>> CRON_START_MAX_BACKOFF =
+      Arg.create(Amount.of(1L, Time.MINUTES));
+
+  // Global per-JVM ID number generator for the provided Quartz Scheduler.
+  private static final AtomicLong ID_GENERATOR = new AtomicLong();
+
+  @Override
+  protected void configure() {
+    bind(CronPredictor.class).to(CronPredictorImpl.class);
+    bind(CronPredictorImpl.class).in(Singleton.class);
+
+    bind(CronJobManager.class).to(CronJobManagerImpl.class);
+    bind(CronJobManagerImpl.class).in(Singleton.class);
+
+    bind(CronScheduler.class).to(CronSchedulerImpl.class);
+    bind(CronSchedulerImpl.class).in(Singleton.class);
+
+    bind(AuroraCronJobFactory.class).in(Singleton.class);
+
+    bind(AuroraCronJob.class).in(Singleton.class);
+    bind(AuroraCronJob.Config.class).toInstance(new AuroraCronJob.Config(
+        new BackoffHelper(CRON_START_INITIAL_BACKOFF.get(), CRON_START_MAX_BACKOFF.get())));
+
+    bind(CronLifecycle.class).in(Singleton.class);
+    PubsubEventModule.bindSubscriber(binder(), CronLifecycle.class);
+  }
+
+  @Provides
+  private TimeZone provideTimeZone() {
+    TimeZone timeZone = TimeZone.getTimeZone(CRON_TIMEZONE.get());
+    TimeZone systemTimeZone = TimeZone.getDefault();
+    if (!timeZone.equals(systemTimeZone)) {
+      LOG.warning("Cron schedules are configured to fire according to timezone "
+          + timeZone.getDisplayName()
+          + " but system timezone is set to "
+          + systemTimeZone.getDisplayName());
+    }
+    return timeZone;
+  }
+
+  /*
+   * NOTE: Quartz implements DirectSchedulerFactory as a mutable global singleton in a static
+   * variable. While the Scheduler instances it produces are independent we synchronize here to
+   * avoid an initialization race across injectors. In practice this only shows up during testing;
+   * production Aurora instances will only have one object graph at a time.
+   */
+  @Provides
+  @Singleton
+  private static synchronized Scheduler provideScheduler(AuroraCronJobFactory jobFactory) {
+    SimpleThreadPool threadPool = new SimpleThreadPool(NUM_THREADS.get(), Thread.NORM_PRIORITY);
+    threadPool.setMakeThreadsDaemons(true);
+
+    DirectSchedulerFactory schedulerFactory = DirectSchedulerFactory.getInstance();
+    String schedulerName = "aurora-cron-" + ID_GENERATOR.incrementAndGet();
+    try {
+      schedulerFactory.createScheduler(schedulerName, schedulerName, threadPool, new RAMJobStore());
+      Scheduler scheduler = schedulerFactory.getScheduler(schedulerName);
+      scheduler.setJobFactory(jobFactory);
+      return scheduler;
+    } catch (SchedulerException e) {
+      LOG.severe("Error initializing Quartz cron scheduler: " + e);
+      throw Throwables.propagate(e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImpl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImpl.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImpl.java
new file mode 100644
index 0000000..fb24c28
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImpl.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import javax.inject.Inject;
+
+import com.twitter.common.util.Clock;
+
+import org.apache.aurora.scheduler.cron.CronPredictor;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.quartz.CronExpression;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+class CronPredictorImpl implements CronPredictor {
+  private final Clock clock;
+  private final TimeZone timeZone;
+
+  @Inject
+  CronPredictorImpl(Clock clock, TimeZone timeZone) {
+    this.clock = checkNotNull(clock);
+    this.timeZone = checkNotNull(timeZone);
+  }
+
+  @Override
+  public Date predictNextRun(CrontabEntry schedule) {
+    CronExpression cronExpression = Quartz.cronExpression(schedule, timeZone);
+    return cronExpression.getNextValidTimeAfter(new Date(clock.nowMillis()));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronSchedulerImpl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronSchedulerImpl.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronSchedulerImpl.java
new file mode 100644
index 0000000..3bef22d
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/CronSchedulerImpl.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+import com.twitter.common.base.Function;
+
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.cron.CronScheduler;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.quartz.CronTrigger;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import static org.apache.aurora.scheduler.cron.quartz.Quartz.jobKey;
+
+class CronSchedulerImpl implements CronScheduler {
+  private static final Logger LOG = Logger.getLogger(CronSchedulerImpl.class.getName());
+
+  private final Scheduler scheduler;
+
+  @Inject
+  CronSchedulerImpl(Scheduler scheduler) {
+    this.scheduler = checkNotNull(scheduler);
+  }
+
+  @Override
+  public Optional<CrontabEntry> getSchedule(IJobKey jobKey) throws IllegalStateException {
+    checkNotNull(jobKey);
+
+    try {
+      return Optional.of(Iterables.getOnlyElement(
+          FluentIterable.from(scheduler.getTriggersOfJob(jobKey(jobKey)))
+              .filter(CronTrigger.class)
+              .transform(new Function<CronTrigger, CrontabEntry>() {
+                @Override
+                public CrontabEntry apply(CronTrigger trigger) {
+                  return Quartz.crontabEntry(trigger);
+                }
+              })));
+    } catch (SchedulerException e) {
+      LOG.log(Level.SEVERE,
+          "Error reading job " + JobKeys.canonicalString(jobKey) + " cronExpression Quartz: " + e,
+          e);
+      return Optional.absent();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/quartz/Quartz.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/quartz/Quartz.java b/src/main/java/org/apache/aurora/scheduler/cron/quartz/Quartz.java
new file mode 100644
index 0000000..63aaade
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/quartz/Quartz.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.TimeZone;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Range;
+
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.quartz.CronExpression;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.CronTrigger;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobKey;
+import org.quartz.TriggerBuilder;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Utilities for converting Aurora datatypes to Quartz datatypes.
+ */
+final class Quartz {
+  private Quartz() {
+    // Utility class.
+  }
+
+  /**
+   * Convert an Aurora CrontabEntry to a Quartz CronExpression.
+   */
+  static CronExpression cronExpression(CrontabEntry entry, TimeZone timeZone) {
+    String dayOfMonth;
+    if (entry.hasWildcardDayOfMonth()) {
+      dayOfMonth = "?"; // special quartz token meaning "don't care"
+    } else {
+      dayOfMonth = entry.getDayOfMonthAsString();
+    }
+    String dayOfWeek;
+    if (entry.hasWildcardDayOfWeek() && !entry.hasWildcardDayOfMonth()) {
+      dayOfWeek = "?";
+    } else {
+      List<Integer> daysOfWeek = Lists.newArrayList();
+      for (Range<Integer> range : entry.getDayOfWeek().asRanges()) {
+        for (int i : ContiguousSet.create(range, DiscreteDomain.integers())) {
+          daysOfWeek.add(i + 1); // Quartz has an off-by-one with what the "standard" defines.
+        }
+      }
+      dayOfWeek = Joiner.on(",").join(daysOfWeek);
+    }
+
+    String rawCronExpresion = Joiner.on(" ").join(
+        "0",
+        entry.getMinuteAsString(),
+        entry.getHourAsString(),
+        dayOfMonth,
+        entry.getMonthAsString(),
+        dayOfWeek);
+    CronExpression cronExpression;
+    try {
+      cronExpression = new CronExpression(rawCronExpresion);
+    } catch (ParseException e) {
+      throw Throwables.propagate(e);
+    }
+    cronExpression.setTimeZone(timeZone);
+    return cronExpression;
+  }
+
+  /**
+   * Convert a Quartz JobKey to an Aurora IJobKey.
+   */
+  static IJobKey auroraJobKey(org.quartz.JobKey jobKey) {
+    return JobKeys.parse(jobKey.getName());
+  }
+
+  /**
+   * Convert an Aurora IJobKey to a Quartz JobKey.
+   */
+  static JobKey jobKey(IJobKey jobKey) {
+    return JobKey.jobKey(JobKeys.canonicalString(jobKey));
+  }
+
+  static CronTrigger cronTrigger(CrontabEntry schedule, TimeZone timeZone) {
+    return TriggerBuilder.newTrigger()
+        .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression(schedule, timeZone)))
+        .withDescription(schedule.toString())
+        .build();
+  }
+
+  static JobDetail jobDetail(IJobKey jobKey, Class<? extends Job> jobClass) {
+    checkNotNull(jobKey);
+    checkNotNull(jobClass);
+
+    return JobBuilder.newJob(jobClass)
+        .withIdentity(jobKey(jobKey))
+        .build();
+  }
+
+  static CrontabEntry crontabEntry(CronTrigger cronTrigger) {
+    return CrontabEntry.parse(cronTrigger.getDescription());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java b/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
deleted file mode 100644
index 61b01d2..0000000
--- a/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron.testing;
-
-import java.io.InputStreamReader;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.twitter.common.util.Clock;
-import com.twitter.common.util.testing.FakeClock;
-
-import org.apache.aurora.scheduler.cron.CronPredictor;
-import org.apache.aurora.scheduler.cron.CronScheduler;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Abstract test to verify conformance with the {@link CronScheduler} interface.
- */
-public abstract class AbstractCronIT {
-  private static final String WILDCARD_SCHEDULE = "* * * * *";
-
-  /**
-   * Child should return an instance of the {@link CronScheduler} test under test here.
-   */
-  protected abstract CronScheduler makeCronScheduler() throws Exception;
-
-  /**
-   * Child should return an instance of the {@link CronPredictor} under test here.
-   *
-   * @param clock The clock the predictor should use.
-   */
-  protected abstract CronPredictor makeCronPredictor(Clock clock) throws Exception;
-
-  @Test
-  public void testCronSchedulerLifecycle() throws Exception {
-    CronScheduler scheduler = makeCronScheduler();
-
-    scheduler.startAsync().awaitRunning();
-    final CountDownLatch cronRan = new CountDownLatch(1);
-    scheduler.schedule(WILDCARD_SCHEDULE, new Runnable() {
-      @Override public void run() {
-        cronRan.countDown();
-      }
-    });
-    cronRan.await();
-    scheduler.stopAsync().awaitTerminated();
-  }
-
-  @Test
-  public void testCronPredictorConforms() throws Exception {
-    FakeClock clock = new FakeClock();
-    CronPredictor cronPredictor = makeCronPredictor(clock);
-
-    for (TriggerPrediction triggerPrediction : getExpectedTriggerPredictions()) {
-      List<Long> results = Lists.newArrayList();
-      clock.setNowMillis(0);
-      for (int i = 0; i < triggerPrediction.getTriggerTimes().size(); i++) {
-        Date nextTriggerTime = cronPredictor.predictNextRun(triggerPrediction.getSchedule());
-        results.add(nextTriggerTime.getTime());
-        clock.setNowMillis(nextTriggerTime.getTime());
-      }
-      assertEquals("Cron schedule "
-          + triggerPrediction.getSchedule()
-          + " should have have predicted trigger times "
-          + triggerPrediction.getTriggerTimes()
-          + " but predicted "
-          + results
-          + " instead.", triggerPrediction.getTriggerTimes(), results);
-    }
-  }
-
-  @Test
-  public void testCronScheduleValidatorAcceptsValidSchedules() throws Exception {
-    CronScheduler cron = makeCronScheduler();
-
-    for (TriggerPrediction triggerPrediction : getExpectedTriggerPredictions()) {
-      assertTrue("Cron schedule " + triggerPrediction.getSchedule() + " should pass validation.",
-          cron.isValidSchedule(triggerPrediction.getSchedule()));
-    }
-  }
-
-  private static List<TriggerPrediction> getExpectedTriggerPredictions() {
-    return new Gson()
-        .fromJson(
-            new InputStreamReader(
-                AbstractCronIT.class.getResourceAsStream("cron-schedule-predictions.json")),
-            new TypeToken<List<TriggerPrediction>>() { }.getType());
-  }
-
-  /**
-   * A schedule and the expected iteratively-applied prediction results.
-   */
-  public static class TriggerPrediction {
-    private String schedule;
-    private List<Long> triggerTimes;
-
-    private TriggerPrediction() {
-      // GSON constructor.
-    }
-
-    public TriggerPrediction(String schedule, List<Long> triggerTimes) {
-      this.schedule = schedule;
-      this.triggerTimes = triggerTimes;
-    }
-
-    public String getSchedule() {
-      return schedule;
-    }
-
-    public List<Long> getTriggerTimes() {
-      return ImmutableList.copyOf(triggerTimes);
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/java/org/apache/aurora/scheduler/http/Cron.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/Cron.java b/src/main/java/org/apache/aurora/scheduler/http/Cron.java
index 80a398a..d8c44f8 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/Cron.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/Cron.java
@@ -26,7 +26,7 @@ import javax.ws.rs.core.Response;
 
 import com.google.common.collect.ImmutableMap;
 
-import org.apache.aurora.scheduler.state.CronJobManager;
+import org.apache.aurora.scheduler.cron.CronJobManager;
 
 /**
  * HTTP interface to dump state of the internal cron scheduler.
@@ -50,7 +50,6 @@ public class Cron {
   public Response dumpContents() {
     Map<String, Object> response = ImmutableMap.<String, Object>builder()
         .put("scheduled", cronManager.getScheduledJobs())
-        .put("pending", cronManager.getPendingRuns())
         .build();
 
    return Response.ok(response).build();


[2/5] CronScheduler based on Quartz

Posted by ke...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobTest.java b/src/test/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobTest.java
new file mode 100644
index 0000000..e7d1c14
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/quartz/AuroraCronJobTest.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Map;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import com.twitter.common.base.Supplier;
+import com.twitter.common.testing.easymock.EasyMockTest;
+import com.twitter.common.util.BackoffHelper;
+
+import org.apache.aurora.gen.AssignedTask;
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.gen.ScheduleStatus;
+import org.apache.aurora.gen.ScheduledTask;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.state.StateManager;
+import org.apache.aurora.scheduler.storage.Storage;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
+import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
+import org.apache.aurora.scheduler.storage.mem.MemStorage;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.quartz.JobExecutionException;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class AuroraCronJobTest extends EasyMockTest {
+  public static final String TASK_ID = "A";
+  private Storage storage;
+  private StateManager stateManager;
+  private CronJobManager cronJobManager;
+  private BackoffHelper backoffHelper;
+
+  private AuroraCronJob auroraCronJob;
+
+  private static final String MANAGER_ID = "MANAGER_ID";
+
+  @Before
+  public void setUp() {
+    storage = MemStorage.newEmptyStorage();
+    stateManager = createMock(StateManager.class);
+    cronJobManager = createMock(CronJobManager.class);
+    backoffHelper = createMock(BackoffHelper.class);
+
+    auroraCronJob = new AuroraCronJob(
+        new AuroraCronJob.Config(backoffHelper), storage, stateManager, cronJobManager);
+    expect(cronJobManager.getManagerKey()).andStubReturn(MANAGER_ID);
+  }
+
+  @Test
+  public void testExecuteNonexistentIsNoop() throws JobExecutionException {
+    control.replay();
+
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test
+  public void testInvalidConfigIsNoop() throws JobExecutionException {
+    control.replay();
+    storage.write(new Storage.MutateWork.NoResult.Quiet() {
+      @Override
+      protected void execute(Storage.MutableStoreProvider storeProvider) {
+        storeProvider.getJobStore().saveAcceptedJob(
+            MANAGER_ID,
+            IJobConfiguration.build(QuartzTestUtil.JOB.newBuilder().setCronSchedule(null)));
+      }
+    });
+
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test
+  public void testEmptyStorage() throws JobExecutionException {
+    stateManager.insertPendingTasks(EasyMock.<Map<Integer, ITaskConfig>>anyObject());
+    expectLastCall().times(3);
+
+    control.replay();
+    populateStorage(CronCollisionPolicy.CANCEL_NEW);
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+    storage = MemStorage.newEmptyStorage();
+
+    populateStorage(CronCollisionPolicy.KILL_EXISTING);
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+    storage = MemStorage.newEmptyStorage();
+
+    populateStorage(CronCollisionPolicy.RUN_OVERLAP);
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test
+  public void testCancelNew() throws JobExecutionException {
+    control.replay();
+
+    populateTaskStore();
+    populateStorage(CronCollisionPolicy.CANCEL_NEW);
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test
+  public void testKillExisting() throws Exception {
+    Capture<Supplier<Boolean>> capture = createCapture();
+
+    expect(stateManager.changeState(
+        TASK_ID,
+        Optional.<ScheduleStatus>absent(),
+        ScheduleStatus.KILLING,
+        AuroraCronJob.KILL_AUDIT_MESSAGE))
+        .andReturn(true);
+    backoffHelper.doUntilSuccess(EasyMock.capture(capture));
+    stateManager.insertPendingTasks(EasyMock.<Map<Integer, ITaskConfig>>anyObject());
+
+    control.replay();
+
+    populateStorage(CronCollisionPolicy.KILL_EXISTING);
+    populateTaskStore();
+    auroraCronJob.doExecute(QuartzTestUtil.AURORA_JOB_KEY);
+    assertFalse(capture.getValue().get());
+    storage.write(
+        new Storage.MutateWork.NoResult.Quiet() {
+          @Override
+          protected void execute(Storage.MutableStoreProvider storeProvider) {
+            storeProvider.getUnsafeTaskStore().deleteAllTasks();
+          }
+        });
+    assertTrue(capture.getValue().get());
+  }
+
+  private void populateTaskStore() {
+    storage.write(new Storage.MutateWork.NoResult.Quiet() {
+      @Override
+      protected void execute(Storage.MutableStoreProvider storeProvider) {
+        storeProvider.getUnsafeTaskStore().saveTasks(ImmutableSet.of(
+            IScheduledTask.build(new ScheduledTask()
+                .setStatus(ScheduleStatus.RUNNING)
+                .setAssignedTask(new AssignedTask()
+                    .setTaskId(TASK_ID)
+                    .setTask(QuartzTestUtil.JOB.getTaskConfig().newBuilder())))
+        ));
+      }
+    });
+  }
+
+  private void populateStorage(final CronCollisionPolicy policy) {
+    storage.write(new Storage.MutateWork.NoResult.Quiet() {
+      @Override
+      public void execute(Storage.MutableStoreProvider storeProvider) {
+        storeProvider.getJobStore().saveAcceptedJob(
+            MANAGER_ID,
+            QuartzTestUtil.makeSanitizedCronJob(policy).getSanitizedConfig().getJobConfig());
+      }
+    });
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronIT.java b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronIT.java
new file mode 100644
index 0000000..21e8278
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronIT.java
@@ -0,0 +1,257 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.concurrent.CountDownLatch;
+
+import com.google.common.util.concurrent.Service;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import com.google.inject.util.Modules;
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.base.ExceptionalCommand;
+import com.twitter.common.testing.easymock.EasyMockTest;
+import com.twitter.common.util.Clock;
+
+import org.apache.aurora.gen.ExecutorConfig;
+import org.apache.aurora.gen.Identity;
+import org.apache.aurora.gen.JobConfiguration;
+import org.apache.aurora.gen.TaskConfig;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.events.EventSink;
+import org.apache.aurora.scheduler.events.PubsubEvent;
+import org.apache.aurora.scheduler.state.PubsubTestUtil;
+import org.apache.aurora.scheduler.state.StateManager;
+import org.apache.aurora.scheduler.storage.Storage;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.mem.MemStorage;
+import org.easymock.Capture;
+import org.easymock.IAnswer;
+import org.junit.Before;
+import org.junit.Test;
+import org.quartz.JobExecutionContext;
+import org.quartz.Scheduler;
+import org.quartz.Trigger;
+import org.quartz.TriggerListener;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.isA;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CronIT extends EasyMockTest {
+  public static final CrontabEntry CRONTAB_ENTRY = CrontabEntry.parse("* * * * *");
+
+  private static final IJobKey JOB_KEY = JobKeys.from("roll", "b", "c");
+  private static final Identity IDENTITY = new Identity()
+      .setRole(JOB_KEY.getRole())
+      .setUser("user");
+
+  private static final IJobConfiguration CRON_JOB = IJobConfiguration.build(
+      new JobConfiguration()
+          .setCronSchedule(CRONTAB_ENTRY.toString())
+          .setKey(JOB_KEY.newBuilder())
+          .setInstanceCount(2)
+          .setOwner(IDENTITY)
+          .setTaskConfig(new TaskConfig()
+              .setJobName(JOB_KEY.getName())
+              .setEnvironment(JOB_KEY.getEnvironment())
+              .setOwner(IDENTITY)
+              .setExecutorConfig(new ExecutorConfig()
+                  .setName("cmd.exe")
+                  .setData("echo hello world"))
+              .setNumCpus(7)
+              .setRamMb(8)
+              .setDiskMb(9))
+  );
+
+  private ShutdownRegistry shutdownRegistry;
+  private EventSink eventSink;
+  private Injector injector;
+  private StateManager stateManager;
+  private Storage storage;
+  private AuroraCronJob auroraCronJob;
+
+  private Capture<ExceptionalCommand<?>> shutdown;
+
+  @Before
+  public void setUp() throws Exception {
+    shutdownRegistry = createMock(ShutdownRegistry.class);
+    stateManager = createMock(StateManager.class);
+    storage = MemStorage.newEmptyStorage();
+    auroraCronJob = createMock(AuroraCronJob.class);
+
+    injector = Guice.createInjector(
+        // Override to verify that Guice is actually used for construction of the AuroraCronJob.
+        // TODO(ksweeney): Use the production class here.
+        Modules.override(new CronModule()).with(new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(AuroraCronJob.class).toInstance(auroraCronJob);
+          }
+        }), new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(Clock.class).toInstance(Clock.SYSTEM_CLOCK);
+            bind(ShutdownRegistry.class).toInstance(shutdownRegistry);
+            bind(StateManager.class).toInstance(stateManager);
+            bind(Storage.class).toInstance(storage);
+
+            PubsubTestUtil.installPubsub(binder());
+          }
+        });
+    eventSink = PubsubTestUtil.startPubsub(injector);
+
+    shutdown = createCapture();
+    shutdownRegistry.addAction(capture(shutdown));
+  }
+
+  private void boot() {
+    eventSink.post(new PubsubEvent.SchedulerActive());
+  }
+
+  @Test
+  public void testCronSchedulerLifecycle() throws Exception {
+    control.replay();
+
+    Scheduler scheduler = injector.getInstance(Scheduler.class);
+    assertTrue(!scheduler.isStarted());
+
+    boot();
+    Service cronLifecycle = injector.getInstance(CronLifecycle.class);
+
+    assertTrue(cronLifecycle.isRunning());
+    assertTrue(scheduler.isStarted());
+
+    shutdown.getValue().execute();
+
+    assertTrue(!cronLifecycle.isRunning());
+    assertTrue(scheduler.isShutdown());
+  }
+
+  @Test
+  public void testJobsAreScheduled() throws Exception {
+    auroraCronJob.execute(isA(JobExecutionContext.class));
+
+    control.replay();
+    final CronJobManager cronJobManager = injector.getInstance(CronJobManager.class);
+    final Scheduler scheduler = injector.getInstance(Scheduler.class);
+
+    storage.write(new Storage.MutateWork.NoResult.Quiet() {
+      @Override
+      public void execute(Storage.MutableStoreProvider storeProvider) {
+        storeProvider.getJobStore().saveAcceptedJob(
+            cronJobManager.getManagerKey(),
+            CRON_JOB);
+      }
+    });
+
+    final CountDownLatch cronRan = new CountDownLatch(1);
+    scheduler.getListenerManager().addTriggerListener(new CountDownWhenComplete(cronRan));
+    boot();
+
+    cronRan.await();
+
+    shutdown.getValue().execute();
+  }
+
+  @Test
+  public void testKillExistingDogpiles() throws Exception {
+    // Test that a trigger for a job that hasn't finished running is ignored.
+    final CronJobManager cronJobManager = injector.getInstance(CronJobManager.class);
+
+    final CountDownLatch firstExecutionTriggered = new CountDownLatch(1);
+    final CountDownLatch firstExecutionCompleted = new CountDownLatch(1);
+    final CountDownLatch secondExecutionTriggered = new CountDownLatch(1);
+    final CountDownLatch secondExecutionCompleted = new CountDownLatch(1);
+
+    auroraCronJob.execute(isA(JobExecutionContext.class));
+    expectLastCall().andAnswer(new IAnswer<Void>() {
+      @Override
+      public Void answer() throws Throwable {
+        firstExecutionTriggered.countDown();
+        firstExecutionCompleted.await();
+        return null;
+      }
+    });
+    auroraCronJob.execute(isA(JobExecutionContext.class));
+    expectLastCall().andAnswer(new IAnswer<Object>() {
+      @Override
+      public Void answer() throws Throwable {
+        secondExecutionTriggered.countDown();
+        secondExecutionCompleted.await();
+        return null;
+      }
+    });
+
+    control.replay();
+
+    boot();
+
+    cronJobManager.createJob(SanitizedCronJob.fromUnsanitized(CRON_JOB));
+    cronJobManager.startJobNow(JOB_KEY);
+    firstExecutionTriggered.await();
+    cronJobManager.startJobNow(JOB_KEY);
+    assertEquals(1, secondExecutionTriggered.getCount());
+    firstExecutionCompleted.countDown();
+    secondExecutionTriggered.await();
+    secondExecutionTriggered.countDown();
+  }
+
+  private static class CountDownWhenComplete implements TriggerListener {
+    private final CountDownLatch countDownLatch;
+
+    CountDownWhenComplete(CountDownLatch countDownLatch) {
+      this.countDownLatch = countDownLatch;
+    }
+
+    @Override
+    public String getName() {
+      return CountDownWhenComplete.class.getName();
+    }
+
+    @Override
+    public void triggerFired(Trigger trigger, JobExecutionContext context) {
+    }
+
+    @Override
+    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
+      return false;
+    }
+
+    @Override
+    public void triggerMisfired(Trigger trigger) {
+      // No-op.
+    }
+
+    @Override
+    public void triggerComplete(
+        Trigger trigger,
+        JobExecutionContext context,
+        Trigger.CompletedExecutionInstruction triggerInstructionCode) {
+
+      countDownLatch.countDown();
+      // No-op.
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImplTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImplTest.java b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImplTest.java
new file mode 100644
index 0000000..b9116a4
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronJobManagerImplTest.java
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.TimeZone;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.twitter.common.testing.easymock.EasyMockTest;
+
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.storage.Storage;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.mem.MemStorage;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.quartz.JobDetail;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.Trigger;
+import org.quartz.impl.matchers.GroupMatcher;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class CronJobManagerImplTest extends EasyMockTest {
+  private Storage storage;
+  private Scheduler scheduler;
+
+  private CronJobManager cronJobManager;
+
+  @Before
+  public void setUp() {
+    storage = MemStorage.newEmptyStorage();
+    scheduler = createMock(Scheduler.class);
+
+    cronJobManager = new CronJobManagerImpl(storage, scheduler, TimeZone.getTimeZone("GMT"));
+  }
+
+  @Test
+  public void testStartJobNowExistent() throws Exception {
+    populateStorage();
+    scheduler.triggerJob(QuartzTestUtil.QUARTZ_JOB_KEY);
+
+    control.replay();
+
+    cronJobManager.startJobNow(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test(expected = CronException.class)
+  public void testStartJobNowNonexistent() throws Exception {
+    control.replay();
+
+    cronJobManager.startJobNow(QuartzTestUtil.AURORA_JOB_KEY);
+  }
+
+  @Test
+  public void testUpdateExistingJob() throws Exception {
+    SanitizedCronJob sanitizedCronJob = QuartzTestUtil.makeSanitizedCronJob();
+
+    expect(scheduler.deleteJob(QuartzTestUtil.QUARTZ_JOB_KEY)).andReturn(true);
+    expect(scheduler.scheduleJob(anyObject(JobDetail.class), anyObject(Trigger.class)))
+       .andReturn(null);
+
+    populateStorage();
+
+    control.replay();
+
+    cronJobManager.updateJob(sanitizedCronJob);
+    assertEquals(sanitizedCronJob.getSanitizedConfig().getJobConfig(), fetchFromStorage().orNull());
+  }
+
+  @Test
+  public void testUpdateNonexistentJob() throws Exception {
+    control.replay();
+
+    try {
+      cronJobManager.updateJob(QuartzTestUtil.makeUpdatedJob());
+      fail();
+    } catch (CronException e) {
+      // Expected.
+    }
+
+    assertEquals(Optional.<IJobConfiguration>absent(), fetchFromStorage());
+  }
+
+  @Test
+  public void testCreateNonexistentJob() throws Exception {
+    SanitizedCronJob sanitizedCronJob = QuartzTestUtil.makeSanitizedCronJob();
+
+    expect(scheduler.scheduleJob(anyObject(JobDetail.class), anyObject(Trigger.class)))
+        .andReturn(null);
+
+    control.replay();
+
+    cronJobManager.createJob(sanitizedCronJob);
+
+    assertEquals(
+        sanitizedCronJob.getSanitizedConfig().getJobConfig(),
+        fetchFromStorage().orNull());
+  }
+
+  @Test(expected = CronException.class)
+  public void testCreateExistingJobFails() throws Exception {
+    SanitizedCronJob sanitizedCronJob = QuartzTestUtil.makeSanitizedCronJob();
+    populateStorage();
+    control.replay();
+
+    cronJobManager.createJob(sanitizedCronJob);
+  }
+
+  @Test
+  public void testGetJobs() throws Exception {
+    control.replay();
+    assertEquals(Collections.emptyList(), ImmutableList.copyOf(cronJobManager.getJobs()));
+
+    populateStorage();
+    assertEquals(
+        QuartzTestUtil.makeSanitizedCronJob().getSanitizedConfig().getJobConfig(),
+        Iterables.getOnlyElement(cronJobManager.getJobs()));
+  }
+
+  @Test
+  public void testNoRunOverlap() throws Exception {
+    SanitizedCronJob runOverlapJob = SanitizedCronJob.fromUnsanitized(
+        IJobConfiguration.build(QuartzTestUtil.JOB.newBuilder()
+            .setCronCollisionPolicy(CronCollisionPolicy.RUN_OVERLAP)));
+
+    control.replay();
+
+    try {
+      cronJobManager.createJob(runOverlapJob);
+      fail();
+    } catch (CronException e) {
+      // Expected.
+    }
+
+    try {
+      cronJobManager.updateJob(runOverlapJob);
+    } catch (CronException e) {
+      // Expected.
+    }
+
+    assertEquals(Optional.<IJobConfiguration>absent(), fetchFromStorage());
+  }
+
+  @Test
+  public void testDeleteJob() throws Exception {
+    expect(scheduler.deleteJob(QuartzTestUtil.QUARTZ_JOB_KEY)).andReturn(true);
+
+    control.replay();
+
+    assertFalse(cronJobManager.deleteJob(QuartzTestUtil.AURORA_JOB_KEY));
+    populateStorage();
+    assertTrue(cronJobManager.deleteJob(QuartzTestUtil.AURORA_JOB_KEY));
+    assertEquals(Optional.<IJobConfiguration>absent(), fetchFromStorage());
+  }
+
+  @Test
+  public void testGetScheduledJobs() throws Exception {
+    JobDetail jobDetail = createMock(JobDetail.class);
+    expect(scheduler.getJobKeys(EasyMock.<GroupMatcher<JobKey>>anyObject()))
+        .andReturn(ImmutableSet.of(QuartzTestUtil.QUARTZ_JOB_KEY));
+    expect(scheduler.getJobDetail(QuartzTestUtil.QUARTZ_JOB_KEY))
+        .andReturn(jobDetail);
+    expect(jobDetail.getDescription()).andReturn("* * * * *");
+
+    control.replay();
+
+    Map<IJobKey, CrontabEntry> scheduledJobs = cronJobManager.getScheduledJobs();
+    assertEquals(CrontabEntry.parse("* * * * *"), scheduledJobs.get(QuartzTestUtil.AURORA_JOB_KEY));
+  }
+
+  private void populateStorage() throws Exception {
+    storage.write(new Storage.MutateWork.NoResult<Exception>() {
+      @Override
+      protected void execute(Storage.MutableStoreProvider storeProvider) throws Exception {
+        storeProvider.getJobStore().saveAcceptedJob(
+            cronJobManager.getManagerKey(),
+            QuartzTestUtil.makeSanitizedCronJob().getSanitizedConfig().getJobConfig());
+      }
+    });
+  }
+
+  private Optional<IJobConfiguration> fetchFromStorage() {
+    return storage.consistentRead(new Storage.Work.Quiet<Optional<IJobConfiguration>>() {
+      @Override
+      public Optional<IJobConfiguration> apply(Storage.StoreProvider storeProvider) {
+        return storeProvider.getJobStore().fetchJob(cronJobManager.getManagerKey(),
+            QuartzTestUtil.AURORA_JOB_KEY);
+      }
+    });
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImplTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImplTest.java b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImplTest.java
new file mode 100644
index 0000000..6bc97f3
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/quartz/CronPredictorImplTest.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.twitter.common.quantity.Amount;
+import com.twitter.common.quantity.Time;
+import com.twitter.common.util.testing.FakeClock;
+
+import org.apache.aurora.scheduler.cron.CronPredictor;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+
+import org.apache.aurora.scheduler.cron.ExpectedPrediction;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class CronPredictorImplTest {
+  private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("GMT");
+  public static final CrontabEntry CRONTAB_ENTRY = CrontabEntry.parse("* * * * *");
+
+  private CronPredictor cronPredictor;
+
+  private FakeClock clock;
+
+  @Before
+  public void setUp() {
+    clock = new FakeClock();
+    cronPredictor = new CronPredictorImpl(clock, TIME_ZONE);
+  }
+
+  @Test
+  public void testValidSchedule() {
+    clock.advance(Amount.of(1L, Time.DAYS));
+    Date expectedPrediction = new Date(Amount.of(1L, Time.DAYS).as(Time.MILLISECONDS)
+            + Amount.of(1L, Time.MINUTES).as(Time.MILLISECONDS));
+    assertEquals(expectedPrediction, cronPredictor.predictNextRun(CrontabEntry.parse("* * * * *")));
+  }
+
+  @Test
+  public void testCronExpressions() {
+    assertEquals("0 * * ? * 1,2,3,4,5,6,7",
+        Quartz.cronExpression(CRONTAB_ENTRY, TIME_ZONE).getCronExpression());
+  }
+
+  @Test
+  public void testCronPredictorConforms() throws Exception {
+    for (ExpectedPrediction expectedPrediction : ExpectedPrediction.getAll()) {
+      List<Date> results = Lists.newArrayList();
+      clock.setNowMillis(0);
+      for (int i = 0; i < expectedPrediction.getTriggerTimes().size(); i++) {
+        Date nextTriggerTime = cronPredictor.predictNextRun(expectedPrediction.parseCrontabEntry());
+        results.add(nextTriggerTime);
+        clock.setNowMillis(nextTriggerTime.getTime());
+      }
+      assertEquals(
+          "Cron schedule " + expectedPrediction.getSchedule() + " made unexpected predictions.",
+          Lists.transform(
+              expectedPrediction.getTriggerTimes(),
+              new Function<Long, Date>() {
+                @Override
+                public Date apply(Long time) {
+                  return new Date(time);
+                }
+              }
+          ),
+          results);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/quartz/QuartzTestUtil.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/quartz/QuartzTestUtil.java b/src/test/java/org/apache/aurora/scheduler/cron/quartz/QuartzTestUtil.java
new file mode 100644
index 0000000..b10f514
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/quartz/QuartzTestUtil.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron.quartz;
+
+import com.google.common.base.Throwables;
+
+import org.apache.aurora.gen.CronCollisionPolicy;
+import org.apache.aurora.gen.ExecutorConfig;
+import org.apache.aurora.gen.Identity;
+import org.apache.aurora.gen.JobConfiguration;
+import org.apache.aurora.gen.TaskConfig;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.configuration.ConfigurationManager;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
+import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.quartz.JobKey;
+
+/**
+ * Fixtures used across quartz tests.
+ */
+final class QuartzTestUtil {
+  static final IJobKey AURORA_JOB_KEY = JobKeys.from("role", "env", "job");
+  static final IJobConfiguration JOB = IJobConfiguration.build(
+      new JobConfiguration()
+          .setCronSchedule("* * * * SUN")
+          .setInstanceCount(10)
+          .setOwner(new Identity("role", "user"))
+          .setKey(AURORA_JOB_KEY.newBuilder())
+          .setTaskConfig(new TaskConfig()
+              .setOwner(new Identity("role", "user"))
+              .setJobName(AURORA_JOB_KEY.getName())
+              .setEnvironment(AURORA_JOB_KEY.getEnvironment())
+              .setDiskMb(3)
+              .setRamMb(4)
+              .setNumCpus(5)
+              .setExecutorConfig(new ExecutorConfig()
+                  .setName("cmd.exe")
+                  .setData("echo hello world")))
+  );
+  static final JobKey QUARTZ_JOB_KEY = Quartz.jobKey(AURORA_JOB_KEY);
+
+  private QuartzTestUtil() {
+    // Utility class.
+  }
+
+  static SanitizedCronJob makeSanitizedCronJob(CronCollisionPolicy collisionPolicy) {
+    try {
+      return SanitizedCronJob.fromUnsanitized(
+          IJobConfiguration.build(JOB.newBuilder().setCronCollisionPolicy(collisionPolicy)));
+    } catch (CronException | ConfigurationManager.TaskDescriptionException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  static SanitizedCronJob makeSanitizedCronJob() {
+    return makeSanitizedCronJob(CronCollisionPolicy.KILL_EXISTING);
+  }
+
+  static SanitizedCronJob makeUpdatedJob() throws Exception {
+    return SanitizedCronJob.fromUnsanitized(
+        IJobConfiguration.build(JOB.newBuilder().setCronSchedule("* * 1 * *")));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/state/BaseSchedulerCoreImplTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/state/BaseSchedulerCoreImplTest.java b/src/test/java/org/apache/aurora/scheduler/state/BaseSchedulerCoreImplTest.java
index da6c0ff..0e17f49 100644
--- a/src/test/java/org/apache/aurora/scheduler/state/BaseSchedulerCoreImplTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/state/BaseSchedulerCoreImplTest.java
@@ -38,15 +38,12 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.twitter.common.application.ShutdownRegistry;
 import com.twitter.common.collections.Pair;
 import com.twitter.common.testing.easymock.EasyMockTest;
 import com.twitter.common.util.testing.FakeClock;
 
 import org.apache.aurora.gen.AssignedTask;
 import org.apache.aurora.gen.Constraint;
-import org.apache.aurora.gen.CronCollisionPolicy;
 import org.apache.aurora.gen.ExecutorConfig;
 import org.apache.aurora.gen.Identity;
 import org.apache.aurora.gen.JobConfiguration;
@@ -68,7 +65,10 @@ import org.apache.aurora.scheduler.base.Tasks;
 import org.apache.aurora.scheduler.configuration.ConfigurationManager;
 import org.apache.aurora.scheduler.configuration.ConfigurationManager.TaskDescriptionException;
 import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
-import org.apache.aurora.scheduler.cron.CronScheduler;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
 import org.apache.aurora.scheduler.events.EventSink;
 import org.apache.aurora.scheduler.events.PubsubEvent;
 import org.apache.aurora.scheduler.quota.QuotaCheckResult;
@@ -85,6 +85,7 @@ import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
 import org.apache.aurora.scheduler.storage.entities.ITaskEvent;
 import org.apache.mesos.Protos.SlaveID;
 import org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
 import org.easymock.IExpectationSetters;
 import org.junit.Before;
 import org.junit.Test;
@@ -106,9 +107,9 @@ import static org.apache.aurora.scheduler.quota.QuotaCheckResult.Result.INSUFFIC
 import static org.apache.aurora.scheduler.quota.QuotaCheckResult.Result.SUFFICIENT_QUOTA;
 import static org.easymock.EasyMock.anyInt;
 import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.reportMatcher;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -128,25 +129,23 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   private static final IJobKey KEY_A = JobKeys.from(ROLE_A, ENV_A, JOB_A);
   private static final int ONE_GB = 1024;
 
-  private static final String ROLE_B = "Test_Role_B";
-  private static final IJobKey KEY_B = JobKeys.from(ROLE_B, ENV_A, JOB_A);
-
   private static final SlaveID SLAVE_ID = SlaveID.newBuilder().setValue("SlaveId").build();
   private static final String SLAVE_HOST_1 = "SlaveHost1";
 
   private static final QuotaCheckResult ENOUGH_QUOTA = new QuotaCheckResult(SUFFICIENT_QUOTA);
   private static final QuotaCheckResult NOT_ENOUGH_QUOTA = new QuotaCheckResult(INSUFFICIENT_QUOTA);
 
+  public static final CrontabEntry CRONTAB_ENTRY = CrontabEntry.parse("1 1 1 1 *");
+  public static final String RAW_CRONTAB_ENTRY = CRONTAB_ENTRY.toString();
+
   private Driver driver;
   private StateManagerImpl stateManager;
   private Storage storage;
   private SchedulerCoreImpl scheduler;
-  private CronScheduler cronScheduler;
-  private CronJobManager cron;
+  private CronJobManager cronJobManager;
   private FakeClock clock;
   private EventSink eventSink;
   private RescheduleCalculator rescheduleCalculator;
-  private ShutdownRegistry shutdownRegistry;
   private QuotaManager quotaManager;
 
   // TODO(William Farner): Set up explicit expectations for calls to generate task IDs.
@@ -164,18 +163,15 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
     clock = new FakeClock();
     eventSink = createMock(EventSink.class);
     rescheduleCalculator = createMock(RescheduleCalculator.class);
-    cronScheduler = createMock(CronScheduler.class);
-    shutdownRegistry = createMock(ShutdownRegistry.class);
+    cronJobManager = createMock(CronJobManager.class);
     quotaManager = createMock(QuotaManager.class);
 
     eventSink.post(EasyMock.<PubsubEvent>anyObject());
     expectLastCall().anyTimes();
-    expect(cronScheduler.schedule(anyObject(String.class), anyObject(Runnable.class)))
-        .andStubReturn("key");
-    expect(cronScheduler.isValidSchedule(anyObject(String.class))).andStubReturn(true);
 
     expect(quotaManager.checkQuota(anyObject(ITaskConfig.class), anyInt()))
         .andStubReturn(ENOUGH_QUOTA);
+    expect(cronJobManager.hasJob(anyObject(IJobKey.class))).andStubReturn(false);
   }
 
   /**
@@ -208,15 +204,9 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
         taskIdGenerator,
         eventSink,
         rescheduleCalculator);
-    cron = new CronJobManager(
-        stateManager,
-        storage,
-        cronScheduler,
-        shutdownRegistry,
-        MoreExecutors.sameThreadExecutor());
     scheduler = new SchedulerCoreImpl(
         storage,
-        cron,
+        cronJobManager,
         stateManager,
         taskIdGenerator,
         quotaManager);
@@ -250,6 +240,19 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
         FluentIterable.from(tasks).transform(Tasks.SCHEDULED_TO_INSTANCE_ID).toSet());
   }
 
+  @Test
+  public void testCreateJobEmptyString() throws Exception {
+    // TODO(ksweeney): Deprecate this as part of AURORA-423.
+
+    control.replay();
+    buildScheduler();
+
+    SanitizedConfiguration job = SanitizedConfiguration.fromUnsanitized(
+        IJobConfiguration.build(makeJob(KEY_A, 1).getJobConfig().newBuilder().setCronSchedule("")));
+    scheduler.createJob(job);
+    assertTaskCount(1);
+  }
+
   private static Constraint dedicatedConstraint(Set<String> values) {
     return new Constraint(DEDICATED_ATTRIBUTE,
         TaskConstraint.value(new ValueConstraint(false, values)));
@@ -272,7 +275,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
     buildScheduler();
 
     TaskConfig newTask = nonProductionTask();
-    newTask.addToConstraints(dedicatedConstraint(ImmutableSet.of(JobKeys.toPath(KEY_A))));
+    newTask.addToConstraints(dedicatedConstraint(ImmutableSet.of(JobKeys.canonicalString(KEY_A))));
     scheduler.createJob(makeJob(KEY_A, newTask));
     assertEquals(PENDING, getOnlyTask(Query.jobScoped(KEY_A)).getStatus());
   }
@@ -445,147 +448,6 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
     scheduler.createJob(makeJob(KEY_A, 1));
   }
 
-  @Test(expected = ScheduleException.class)
-  public void testCreateDuplicateCronJob() throws Exception {
-    SanitizedConfiguration sanitizedConfiguration = makeCronJob(KEY_A, 1, "1 1 1 1 1");
-
-    control.replay();
-    buildScheduler();
-
-    // Cron jobs are scheduled on a delay, so this job's tasks will not be scheduled immediately,
-    // but duplicate jobs should still be rejected.
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-
-    scheduler.createJob(makeJob(KEY_A, 1));
-  }
-
-  @Test
-  public void testStartCronJob() throws Exception {
-    // Create a cron job, ask the scheduler to start it, and ensure that the tasks exist
-    // in the PENDING state.
-
-    SanitizedConfiguration sanitizedConfiguration = makeCronJob(KEY_A, 1, "1 1 1 1 1");
-    IJobKey jobKey = sanitizedConfiguration.getJobConfig().getKey();
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-
-    cron.startJobNow(jobKey);
-    assertEquals(PENDING, getOnlyTask(Query.jobScoped(jobKey)).getStatus());
-  }
-
-  @Test(expected = ScheduleException.class)
-  public void testStartNonexistentCronJob() throws Exception {
-    // Try to start a cron job that doesn't exist.
-    control.replay();
-    buildScheduler();
-
-    cron.startJobNow(KEY_A);
-  }
-
-  @Test
-  public void testStartNonCronJob() throws Exception {
-    // Create a NON cron job and try to start it as though it were a cron job, and ensure that
-    // no cron tasks are created.
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(makeJob(KEY_A, 1));
-    String taskId = Tasks.id(getOnlyTask(Query.jobScoped(KEY_A)));
-
-    try {
-      cron.startJobNow(KEY_A);
-      fail("Start should have failed.");
-    } catch (ScheduleException e) {
-      // Expected.
-    }
-
-    assertEquals(PENDING, getTask(taskId).getStatus());
-    assertFalse(cron.hasJob(KEY_A));
-  }
-
-  @Test(expected = ScheduleException.class)
-  public void testStartNonOwnedCronJob() throws Exception {
-    // Try to start a cron job that is not owned by us.
-    // Should throw an exception.
-
-    SanitizedConfiguration sanitizedConfiguration = makeCronJob(KEY_A, 1, "1 1 1 1 1");
-    IJobConfiguration job = sanitizedConfiguration.getJobConfig();
-    expect(cronScheduler.isValidSchedule(job.getCronSchedule())).andReturn(true);
-    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-
-    cron.startJobNow(KEY_B);
-  }
-
-  @Test
-  public void testStartRunningCronJob() throws Exception {
-    // Start a cron job that is already started by an earlier
-    // call and is PENDING. Make sure it follows the cron collision policy.
-    SanitizedConfiguration sanitizedConfiguration =
-        makeCronJob(KEY_A, 1, "1 1 1 1 1", CronCollisionPolicy.KILL_EXISTING);
-    expect(cronScheduler.schedule(eq(sanitizedConfiguration.getJobConfig().getCronSchedule()),
-        EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-    assertTrue(cron.hasJob(KEY_A));
-
-    cron.startJobNow(KEY_A);
-    assertTaskCount(1);
-
-    String taskId = Tasks.id(getOnlyTask(Query.jobScoped(KEY_A)));
-
-    // Now start the same cron job immediately.
-    cron.startJobNow(KEY_A);
-    assertTaskCount(1);
-    assertEquals(PENDING, getOnlyTask(Query.jobScoped(KEY_A)).getStatus());
-
-    // Make sure the pending job is the new one.
-    String newTaskId = Tasks.id(getOnlyTask(Query.jobScoped(KEY_A)));
-    assertFalse(taskId.equals(newTaskId));
-  }
-
-  @Test
-  public void testKillCreateCronJob() throws Exception {
-    SanitizedConfiguration sanitizedConfiguration = makeCronJob(KEY_A, 1, "1 1 1 1 1");
-    IJobConfiguration job = sanitizedConfiguration.getJobConfig();
-    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-    cronScheduler.deschedule("key");
-
-    SanitizedConfiguration updated = makeCronJob(KEY_A, 1, "1 2 3 4 5");
-    IJobConfiguration updatedJob = updated.getJobConfig();
-    expect(cronScheduler.schedule(eq(updatedJob.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("key2");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTrue(cron.hasJob(KEY_A));
-
-    scheduler.killTasks(Query.jobScoped(KEY_A), OWNER_A.getUser());
-    scheduler.createJob(updated);
-
-    IJobConfiguration stored = Iterables.getOnlyElement(cron.getJobs());
-    assertEquals(updatedJob.getCronSchedule(), stored.getCronSchedule());
-  }
-
   @Test
   public void testKillTask() throws Exception {
     driver.killTask(EasyMock.<String>anyObject());
@@ -638,17 +500,21 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   @Test
   public void testServiceTasksRescheduled() throws Exception {
     int numServiceTasks = 5;
+    IJobKey adhocKey = KEY_A;
+    IJobKey serviceKey = IJobKey.build(
+        adhocKey.newBuilder().setName(adhocKey.getName() + "service"));
 
     expectTaskNotThrottled().times(numServiceTasks);
+    expectNoCronJob(adhocKey);
+    expectNoCronJob(serviceKey);
 
     control.replay();
     buildScheduler();
 
     // Schedule 5 service and 5 non-service tasks.
-    scheduler.createJob(makeJob(KEY_A, numServiceTasks));
+    scheduler.createJob(makeJob(adhocKey, numServiceTasks));
     TaskConfig task = productionTask().setIsService(true);
-    scheduler.createJob(
-        makeJob(IJobKey.build(KEY_A.newBuilder().setName(KEY_A.getName() + "service")), task, 5));
+    scheduler.createJob(makeJob(serviceKey, task, 5));
 
     assertEquals(10, getTasksByStatus(PENDING).size());
     changeStatus(Query.roleScoped(ROLE_A), ASSIGNED, STARTING);
@@ -675,6 +541,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
     int totalFailures = 10;
 
     expectTaskNotThrottled().times(totalFailures);
+    expectNoCronJob(KEY_A);
 
     control.replay();
     buildScheduler();
@@ -733,6 +600,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   @Test
   public void testNoTransitionFromTerminalState() throws Exception {
     expectKillTask(1);
+    expectNoCronJob(KEY_A);
 
     control.replay();
     buildScheduler();
@@ -753,6 +621,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   public void testFailedTaskIncrementsFailureCount() throws Exception {
     int maxFailures = 5;
     expectTaskNotThrottled().times(maxFailures - 1);
+    expect(cronJobManager.hasJob(KEY_A)).andReturn(false);
 
     control.replay();
     buildScheduler();
@@ -785,78 +654,9 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   }
 
   @Test
-  public void testCronJobLifeCycle() throws Exception {
-    SanitizedConfiguration sanitizedConfiguration = makeCronJob(KEY_A, 10, "1 1 1 1 1");
-    IJobConfiguration job = sanitizedConfiguration.getJobConfig();
-    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-    assertTrue(cron.hasJob(KEY_A));
-
-    // Simulate a triggering of the cron job.
-    cron.startJobNow(KEY_A);
-    assertTaskCount(10);
-    assertEquals(10,
-        getTasks(Query.jobScoped(KEY_A).byStatus(PENDING)).size());
-
-    assertTaskCount(10);
-
-    changeStatus(Query.roleScoped(ROLE_A), ASSIGNED, STARTING);
-    assertTaskCount(10);
-    changeStatus(Query.roleScoped(ROLE_A), RUNNING);
-    assertTaskCount(10);
-    changeStatus(Query.roleScoped(ROLE_A), FINISHED);
-  }
-
-  @Test
-  public void testCronNoSuicide() throws Exception {
-    SanitizedConfiguration sanitizedConfiguration =
-        makeCronJob(KEY_A, 10, "1 1 1 1 1", CronCollisionPolicy.KILL_EXISTING);
-    expect(cronScheduler.schedule(eq(sanitizedConfiguration.getJobConfig().getCronSchedule()),
-        EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(sanitizedConfiguration);
-    assertTaskCount(0);
-
-    try {
-      scheduler.createJob(sanitizedConfiguration);
-      fail();
-    } catch (ScheduleException e) {
-      // Expected.
-    }
-    assertTrue(cron.hasJob(KEY_A));
-
-    // Simulate a triggering of the cron job.
-    cron.startJobNow(KEY_A);
-    assertTaskCount(10);
-
-    Set<String> taskIds = Tasks.ids(getTasksOwnedBy(OWNER_A));
-
-    // Simulate a triggering of the cron job.
-    cron.startJobNow(KEY_A);
-    assertTaskCount(10);
-    assertTrue(Sets.intersection(taskIds, Tasks.ids(getTasksOwnedBy(OWNER_A))).isEmpty());
-
-    try {
-      scheduler.createJob(sanitizedConfiguration);
-      fail();
-    } catch (ScheduleException e) {
-      // Expected.
-    }
-    assertTrue(cron.hasJob(KEY_A));
-  }
-
-  @Test
   public void testKillPendingTask() throws Exception {
+    expectNoCronJob(KEY_A);
+
     control.replay();
     buildScheduler();
 
@@ -891,16 +691,12 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
 
   @Test
   public void testKillCronTask() throws Exception {
-    SanitizedConfiguration sanitizedConfiguration =
-        makeCronJob(KEY_A, 1, "1 1 1 1 1", CronCollisionPolicy.KILL_EXISTING);
-    expect(cronScheduler.schedule(eq(sanitizedConfiguration.getJobConfig().getCronSchedule()),
-        EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-    cronScheduler.deschedule("key");
-
+    expect(cronJobManager.hasJob(KEY_A)).andReturn(false);
+    cronJobManager.createJob(anyObject(SanitizedCronJob.class));
+    expect(cronJobManager.deleteJob(KEY_A)).andReturn(true);
     control.replay();
     buildScheduler();
-    scheduler.createJob(makeCronJob(KEY_A, 1, "1 1 1 1 1"));
+    scheduler.createJob(makeCronJob(KEY_A, 1, RAW_CRONTAB_ENTRY));
 
     // This will fail if the cron task could not be found.
     scheduler.killTasks(Query.jobScoped(KEY_A), OWNER_A.getUser());
@@ -910,6 +706,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   public void testLostTaskRescheduled() throws Exception {
     expectKillTask(2);
     expectTaskNotThrottled().times(2);
+    expect(cronJobManager.hasJob(KEY_A)).andReturn(false);
 
     control.replay();
     buildScheduler();
@@ -939,32 +736,10 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   }
 
   @Test
-  public void testKillNotStrictlyJobScoped() throws Exception {
-    // Makes sure that queries that are not strictly job scoped will not remove the job entirely.
-    SanitizedConfiguration config = makeCronJob(KEY_A, 10, "1 1 1 1 1");
-    IJobConfiguration job = config.getJobConfig();
-    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("key");
-    cronScheduler.deschedule("key");
-
-    control.replay();
-    buildScheduler();
-
-    scheduler.createJob(config);
-    assertTrue(cron.hasJob(KEY_A));
-    cron.startJobNow(KEY_A);
-    assertTaskCount(10);
-
-    scheduler.killTasks(Query.instanceScoped(KEY_A, 0), USER_A);
-    assertTaskCount(9);
-    assertTrue(cron.hasJob(KEY_A));
-
-    scheduler.killTasks(Query.jobScoped(KEY_A), USER_A);
-    assertFalse(cron.hasJob(KEY_A));
-  }
-
-  @Test
   public void testKillJob() throws Exception {
+    expectNoCronJob(KEY_A);
+    expect(cronJobManager.deleteJob(KEY_A)).andReturn(false);
+
     control.replay();
     buildScheduler();
 
@@ -1015,6 +790,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   @Test(expected = ScheduleException.class)
   public void testRestartNonexistentShard() throws Exception {
     expectTaskNotThrottled();
+    expectNoCronJob(KEY_A);
 
     control.replay();
     buildScheduler();
@@ -1026,6 +802,8 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
 
   @Test
   public void testRestartPendingShard() throws Exception {
+    expect(cronJobManager.hasJob(KEY_A)).andReturn(false);
+
     control.replay();
     buildScheduler();
 
@@ -1058,6 +836,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   public void testPortResourceResetAfterReschedule() throws Exception {
     expectKillTask(1);
     expectTaskNotThrottled();
+    expect(cronJobManager.hasJob(KEY_A)).andReturn(false);
 
     control.replay();
     buildScheduler();
@@ -1120,6 +899,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
       }
     };
 
+    expectNoCronJob(KEY_A);
     control.replay();
     buildScheduler();
 
@@ -1174,9 +954,8 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
 
     scheduler.addInstances(
         job.getKey(),
-        ImmutableSet.copyOf(
-            ContiguousSet.create(Range.closed(0, SchedulerCoreImpl.MAX_TASKS_PER_JOB.get()),
-                DiscreteDomain.integers())),
+        ContiguousSet.create(Range.closed(0, SchedulerCoreImpl.MAX_TASKS_PER_JOB.get()),
+            DiscreteDomain.integers()),
         job.getTaskConfig());
   }
 
@@ -1225,6 +1004,7 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
         .setOwner(OWNER_A);
 
     ImmutableSet<Integer> instances = ImmutableSet.of(0);
+    expectNoCronJob(KEY_A);
 
     control.replay();
     buildScheduler();
@@ -1235,6 +1015,10 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
     scheduler.addInstances(KEY_A, instances, ITaskConfig.build(newTask));
   }
 
+  private void expectNoCronJob(IJobKey jobKey) throws CronException {
+    expect(cronJobManager.hasJob(jobKey)).andReturn(false);
+  }
+
   private static String getLocalHost() {
     try {
       return InetAddress.getLocalHost().getHostName();
@@ -1265,19 +1049,6 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   private static SanitizedConfiguration makeCronJob(
       IJobKey jobKey,
       int numDefaultTasks,
-      String cronSchedule,
-      CronCollisionPolicy policy) throws TaskDescriptionException {
-
-    return new SanitizedConfiguration(IJobConfiguration.build(
-        makeCronJob(jobKey, numDefaultTasks, cronSchedule)
-            .getJobConfig()
-            .newBuilder()
-            .setCronCollisionPolicy(policy)));
-  }
-
-  private static SanitizedConfiguration makeCronJob(
-      IJobKey jobKey,
-      int numDefaultTasks,
       String cronSchedule) throws TaskDescriptionException {
 
     SanitizedConfiguration job = makeJob(jobKey, numDefaultTasks);
@@ -1401,4 +1172,23 @@ public abstract class BaseSchedulerCoreImplTest extends EasyMockTest {
   public void changeStatus(String taskId, ScheduleStatus status, Optional<String> message) {
     changeStatus(Query.taskScoped(taskId), status, message);
   }
+
+  private SanitizedConfiguration hasJobKey(final IJobKey key) {
+    reportMatcher(new IArgumentMatcher() {
+      @Override
+      public boolean matches(Object item) {
+        if (!(item instanceof SanitizedConfiguration)) {
+          return false;
+        }
+        SanitizedConfiguration configuration = (SanitizedConfiguration) item;
+        return key.equals(configuration.getJobConfig().getKey());
+      }
+
+      @Override
+      public void appendTo(StringBuffer buffer) {
+        buffer.append(key.toString());
+      }
+    });
+    return null;
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/state/CronJobManagerTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/state/CronJobManagerTest.java b/src/test/java/org/apache/aurora/scheduler/state/CronJobManagerTest.java
deleted file mode 100644
index fa9cb75..0000000
--- a/src/test/java/org/apache/aurora/scheduler/state/CronJobManagerTest.java
+++ /dev/null
@@ -1,490 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.state;
-
-import java.util.concurrent.Executor;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.twitter.common.application.ShutdownRegistry;
-import com.twitter.common.base.ExceptionalCommand;
-import com.twitter.common.testing.easymock.EasyMockTest;
-
-import org.apache.aurora.gen.AssignedTask;
-import org.apache.aurora.gen.CronCollisionPolicy;
-import org.apache.aurora.gen.ExecutorConfig;
-import org.apache.aurora.gen.Identity;
-import org.apache.aurora.gen.JobConfiguration;
-import org.apache.aurora.gen.ScheduleStatus;
-import org.apache.aurora.gen.ScheduledTask;
-import org.apache.aurora.gen.TaskConfig;
-import org.apache.aurora.scheduler.base.JobKeys;
-import org.apache.aurora.scheduler.base.Query;
-import org.apache.aurora.scheduler.base.ScheduleException;
-import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
-import org.apache.aurora.scheduler.cron.CronException;
-import org.apache.aurora.scheduler.cron.CronScheduler;
-import org.apache.aurora.scheduler.events.EventSink;
-import org.apache.aurora.scheduler.events.PubsubEvent.SchedulerActive;
-import org.apache.aurora.scheduler.storage.Storage;
-import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
-import org.apache.aurora.scheduler.storage.entities.IJobKey;
-import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
-import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IExpectationSetters;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.apache.aurora.gen.apiConstants.DEFAULT_ENVIRONMENT;
-import static org.apache.aurora.scheduler.state.CronJobManager.MANAGER_KEY;
-import static org.easymock.EasyMock.capture;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class CronJobManagerTest extends EasyMockTest {
-
-  private static final String OWNER = "owner";
-  private static final String ENVIRONMENT = "staging11";
-  private static final String JOB_NAME = "jobName";
-  private static final String DEFAULT_JOB_KEY = "key";
-  private static final String TASK_ID = "id";
-  private static final IScheduledTask TASK = IScheduledTask.build(
-      new ScheduledTask().setAssignedTask(new AssignedTask().setTaskId(TASK_ID)));
-
-  private StateManager stateManager;
-  private Executor delayExecutor;
-  private Capture<Runnable> delayLaunchCapture;
-  private StorageTestUtil storageUtil;
-
-  private CronScheduler cronScheduler;
-  private ShutdownRegistry shutdownRegistry;
-  private CronJobManager cron;
-  private IJobConfiguration job;
-  private SanitizedConfiguration sanitizedConfiguration;
-
-  @Before
-  public void setUp() throws Exception {
-    stateManager = createMock(StateManager.class);
-    delayExecutor = createMock(Executor.class);
-    delayLaunchCapture = createCapture();
-    storageUtil = new StorageTestUtil(this);
-    storageUtil.expectOperations();
-    cronScheduler = createMock(CronScheduler.class);
-    shutdownRegistry = createMock(ShutdownRegistry.class);
-
-    cron = new CronJobManager(
-        stateManager,
-        storageUtil.storage,
-        cronScheduler,
-        shutdownRegistry,
-        delayExecutor);
-    job = makeJob();
-    sanitizedConfiguration = SanitizedConfiguration.fromUnsanitized(job);
-  }
-
-  private void expectJobValidated(IJobConfiguration savedJob) {
-    expect(cronScheduler.isValidSchedule(savedJob.getCronSchedule())).andReturn(true);
-  }
-
-  private void expectJobValidated() {
-    expectJobValidated(sanitizedConfiguration.getJobConfig());
-  }
-
-  private Capture<Runnable> expectJobAccepted(IJobConfiguration savedJob) throws Exception {
-    expectJobValidated(savedJob);
-    storageUtil.jobStore.saveAcceptedJob(MANAGER_KEY, savedJob);
-    Capture<Runnable> jobTriggerCapture = createCapture();
-    expect(cronScheduler.schedule(eq(savedJob.getCronSchedule()), capture(jobTriggerCapture)))
-        .andReturn(DEFAULT_JOB_KEY);
-    return jobTriggerCapture;
-  }
-
-  private void expectJobAccepted() throws Exception {
-    expectJobAccepted(sanitizedConfiguration.getJobConfig());
-  }
-
-  private void expectJobFetch() {
-    expectJobValidated();
-    expect(storageUtil.jobStore.fetchJob(MANAGER_KEY, job.getKey()))
-        .andReturn(Optional.of(sanitizedConfiguration.getJobConfig()));
-  }
-
-  private IExpectationSetters<?> expectActiveTaskFetch(IScheduledTask... activeTasks) {
-    return storageUtil.expectTaskFetch(Query.jobScoped(job.getKey()).active(), activeTasks);
-  }
-
-  @Test
-  public void testPubsubWiring() throws Exception {
-    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
-    cronScheduler.awaitRunning();
-    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());
-    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY))
-        .andReturn(ImmutableList.<IJobConfiguration>of());
-
-    control.replay();
-
-    Injector injector = Guice.createInjector(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(StateManager.class).toInstance(stateManager);
-        bind(Storage.class).toInstance(storageUtil.storage);
-        bind(CronScheduler.class).toInstance(cronScheduler);
-        bind(ShutdownRegistry.class).toInstance(shutdownRegistry);
-        PubsubTestUtil.installPubsub(binder());
-        StateModule.bindCronJobManager(binder());
-      }
-    });
-    cron = injector.getInstance(CronJobManager.class);
-    EventSink eventSink = PubsubTestUtil.startPubsub(injector);
-    eventSink.post(new SchedulerActive());
-  }
-
-  @Test
-  public void testStart() throws Exception {
-    expectJobAccepted();
-    expectJobFetch();
-    expectActiveTaskFetch();
-
-    // Job is executed immediately since there are no existing tasks to kill.
-    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
-    expect(cronScheduler.getSchedule(DEFAULT_JOB_KEY))
-        .andReturn(Optional.of(job.getCronSchedule()))
-        .times(2);
-
-    control.replay();
-
-    assertEquals(ImmutableMap.<IJobKey, String>of(), cron.getScheduledJobs());
-    cron.receiveJob(sanitizedConfiguration);
-    assertEquals(ImmutableMap.of(job.getKey(), job.getCronSchedule()), cron.getScheduledJobs());
-    cron.startJobNow(job.getKey());
-    assertEquals(ImmutableMap.of(job.getKey(), job.getCronSchedule()), cron.getScheduledJobs());
-  }
-
-  @Test
-  public void testDeleteInconsistent() throws Exception {
-    // Tests a case where a job exists in the storage, but is not registered with the cron system.
-
-    expect(storageUtil.jobStore.fetchJob(MANAGER_KEY, job.getKey()))
-        .andReturn(Optional.of(sanitizedConfiguration.getJobConfig()));
-
-    control.replay();
-
-    assertTrue(cron.deleteJob(job.getKey()));
-  }
-
-  private IExpectationSetters<?> expectTaskKilled(String id) {
-    expect(stateManager.changeState(
-        id,
-        Optional.<ScheduleStatus>absent(),
-        ScheduleStatus.KILLING,
-        CronJobManager.KILL_AUDIT_MESSAGE))
-        .andReturn(true);
-    return expectLastCall();
-  }
-
-  @Test
-  public void testDelayedStart() throws Exception {
-    expectJobAccepted();
-    expectJobFetch();
-
-    // Query to test if live tasks exist for the job.
-    expectActiveTaskFetch(TASK);
-
-    // Live tasks exist, so the cron manager must delay the cron launch.
-    delayExecutor.execute(capture(delayLaunchCapture));
-
-    // The cron manager will then try to initiate the kill.
-    expectTaskKilled(TASK_ID);
-
-    // Immediate query and delayed query.
-    expectActiveTaskFetch(TASK).times(2);
-
-    // Simulate the live task disappearing.
-    expectActiveTaskFetch();
-
-    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-    cron.startJobNow(job.getKey());
-    assertEquals(ImmutableSet.of(job.getKey()), cron.getPendingRuns());
-    delayLaunchCapture.getValue().run();
-    assertEquals(ImmutableSet.<IJobKey>of(), cron.getPendingRuns());
-  }
-
-  @Test
-  public void testDelayedStartResets() throws Exception {
-    expectJobAccepted();
-    expectJobFetch();
-
-    // Query to test if live tasks exist for the job.
-    expectActiveTaskFetch(TASK);
-
-    // Live tasks exist, so the cron manager must delay the cron launch.
-    delayExecutor.execute(capture(delayLaunchCapture));
-
-    // The cron manager will then try to initiate the kill.
-    expectTaskKilled(TASK_ID);
-
-    // Immediate query and delayed query.
-    expectActiveTaskFetch(TASK).times(2);
-
-    // Simulate the live task disappearing.
-    expectActiveTaskFetch();
-
-    // Round two.
-    expectJobFetch();
-    expectActiveTaskFetch(TASK);
-    delayExecutor.execute(capture(delayLaunchCapture));
-    expectTaskKilled(TASK_ID);
-    expectActiveTaskFetch(TASK).times(2);
-    expectActiveTaskFetch();
-
-    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
-    expectLastCall().times(2);
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-    cron.startJobNow(job.getKey());
-    delayLaunchCapture.getValue().run();
-
-    // Start the job again.  Since the previous delayed start completed, this should repeat the
-    // entire process.
-    cron.startJobNow(job.getKey());
-    delayLaunchCapture.getValue().run();
-  }
-
-  @Test
-  public void testDelayedStartMultiple() throws Exception {
-    expectJobAccepted();
-    expectJobFetch();
-
-    // Query to test if live tasks exist for the job.
-    expectActiveTaskFetch(TASK).times(3);
-
-    // Live tasks exist, so the cron manager must delay the cron launch.
-    delayExecutor.execute(capture(delayLaunchCapture));
-
-    // The cron manager will then try to initiate the kill.
-    expectJobFetch();
-    expectJobFetch();
-    expectTaskKilled(TASK_ID).times(3);
-
-    // Immediate queries and delayed query.
-    expectActiveTaskFetch(TASK).times(4);
-
-    // Simulate the live task disappearing.
-    expectActiveTaskFetch();
-
-    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-
-    // Attempt to trick the cron manager into launching multiple times, or launching multiple
-    // pollers.
-    cron.startJobNow(job.getKey());
-    cron.startJobNow(job.getKey());
-    cron.startJobNow(job.getKey());
-    delayLaunchCapture.getValue().run();
-  }
-
-  @Test
-  public void testUpdate() throws Exception {
-    SanitizedConfiguration updated = new SanitizedConfiguration(
-        IJobConfiguration.build(job.newBuilder().setCronSchedule("1 2 3 4 5")));
-
-    expectJobAccepted();
-    cronScheduler.deschedule(DEFAULT_JOB_KEY);
-    expectJobAccepted(updated.getJobConfig());
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-    cron.updateJob(updated);
-  }
-
-  @Test
-  public void testConsistentState() throws Exception {
-    IJobConfiguration updated =
-        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5"));
-
-    expectJobAccepted();
-    cronScheduler.deschedule(DEFAULT_JOB_KEY);
-    expectJobAccepted(updated);
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-
-    IJobConfiguration failedUpdate =
-        IJobConfiguration.build(updated.newBuilder().setCronSchedule(null));
-    try {
-      cron.updateJob(new SanitizedConfiguration(failedUpdate));
-      fail();
-    } catch (ScheduleException e) {
-      // Expected.
-    }
-
-    cron.updateJob(new SanitizedConfiguration(updated));
-  }
-
-  @Test
-  public void testInvalidStoredJob() throws Exception {
-    // Invalid jobs are left alone, but doesn't halt operation.
-
-    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
-    cronScheduler.awaitRunning();
-    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());
-
-    IJobConfiguration jobA =
-        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5 6 7"));
-    IJobConfiguration jobB =
-        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule(null));
-
-    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY)).andReturn(ImmutableList.of(jobA, jobB));
-    expect(cronScheduler.isValidSchedule(jobA.getCronSchedule())).andReturn(false);
-
-    control.replay();
-
-    cron.schedulerActive(new SchedulerActive());
-  }
-
-  @Test(expected = IllegalStateException.class)
-  public void testJobStoredTwice() throws Exception {
-    // Simulate an inconsistent storage that contains two cron jobs under the same key.
-
-    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
-    cronScheduler.awaitRunning();
-    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());
-
-    IJobConfiguration jobA =
-        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5"));
-    IJobConfiguration jobB =
-        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("* * * * *"));
-    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY))
-        .andReturn(ImmutableList.of(jobA, jobB));
-    expectJobValidated(jobA);
-    expect(cronScheduler.schedule(eq(jobA.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("keyA");
-    expectJobValidated(jobB);
-
-    control.replay();
-
-    cron.schedulerActive(new SchedulerActive());
-  }
-
-  @Test(expected = ScheduleException.class)
-  public void testInvalidCronSchedule() throws Exception {
-    expect(cronScheduler.isValidSchedule(job.getCronSchedule())).andReturn(false);
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-  }
-
-  @Test
-  public void testCancelNewCollision() throws Exception {
-    IJobConfiguration killExisting = IJobConfiguration.build(
-        job.newBuilder().setCronCollisionPolicy(CronCollisionPolicy.CANCEL_NEW));
-    Capture<Runnable> jobTriggerCapture = expectJobAccepted(killExisting);
-    expectActiveTaskFetch(TASK);
-
-    control.replay();
-
-    cron.receiveJob(new SanitizedConfiguration(killExisting));
-    jobTriggerCapture.getValue().run();
-  }
-
-  @Test(expected = ScheduleException.class)
-  public void testScheduleFails() throws Exception {
-    expectJobValidated(job);
-    storageUtil.jobStore.saveAcceptedJob(MANAGER_KEY, sanitizedConfiguration.getJobConfig());
-    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andThrow(new CronException("injected"));
-
-    control.replay();
-
-    cron.receiveJob(sanitizedConfiguration);
-  }
-
-  @Test(expected = ScheduleException.class)
-  public void testRunOverlapRejected() throws Exception {
-    IJobConfiguration killExisting = IJobConfiguration.build(
-        job.newBuilder().setCronCollisionPolicy(CronCollisionPolicy.RUN_OVERLAP));
-
-    control.replay();
-
-    cron.receiveJob(new SanitizedConfiguration(killExisting));
-  }
-
-  @Test
-  public void testRunOverlapLoadedSuccessfully() throws Exception {
-    // Existing RUN_OVERLAP jobs should still load and map.
-
-    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
-    cronScheduler.awaitRunning();
-    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());
-
-    IJobConfiguration jobA =
-        IJobConfiguration.build(makeJob().newBuilder()
-            .setCronCollisionPolicy(CronCollisionPolicy.RUN_OVERLAP));
-
-    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY)).andReturn(ImmutableList.of(jobA));
-    expect(cronScheduler.isValidSchedule(jobA.getCronSchedule())).andReturn(true);
-    expect(cronScheduler.schedule(eq(jobA.getCronSchedule()), EasyMock.<Runnable>anyObject()))
-        .andReturn("keyA");
-
-    control.replay();
-
-    cron.schedulerActive(new SchedulerActive());
-  }
-
-  private IJobConfiguration makeJob() {
-    return IJobConfiguration.build(new JobConfiguration()
-        .setOwner(new Identity(OWNER, OWNER))
-        .setKey(JobKeys.from(OWNER, ENVIRONMENT, JOB_NAME).newBuilder())
-        .setCronSchedule("1 1 1 1 1")
-        .setTaskConfig(defaultTask())
-        .setInstanceCount(1));
-  }
-
-  private static TaskConfig defaultTask() {
-    return new TaskConfig()
-        .setContactEmail("testing@twitter.com")
-        .setExecutorConfig(new ExecutorConfig("aurora", "data"))
-        .setNumCpus(1)
-        .setRamMb(1024)
-        .setDiskMb(1024)
-        .setJobName(JOB_NAME)
-        .setOwner(new Identity(OWNER, OWNER))
-        .setEnvironment(DEFAULT_ENVIRONMENT);
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/state/LockManagerImplTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/state/LockManagerImplTest.java b/src/test/java/org/apache/aurora/scheduler/state/LockManagerImplTest.java
index c8ad55d..cdb1f5d 100644
--- a/src/test/java/org/apache/aurora/scheduler/state/LockManagerImplTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/state/LockManagerImplTest.java
@@ -133,6 +133,6 @@ public class LockManagerImplTest extends EasyMockTest {
 
   private void expectLockException(IJobKey key) {
     expectedException.expect(LockException.class);
-    expectedException.expectMessage(JobKeys.toPath(key));
+    expectedException.expectMessage(JobKeys.canonicalString(key));
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterfaceTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterfaceTest.java b/src/test/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterfaceTest.java
index 405da0a..2142f11 100644
--- a/src/test/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterfaceTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/thrift/SchedulerThriftInterfaceTest.java
@@ -76,10 +76,13 @@ import org.apache.aurora.scheduler.base.Query;
 import org.apache.aurora.scheduler.base.ScheduleException;
 import org.apache.aurora.scheduler.configuration.ConfigurationManager;
 import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
+import org.apache.aurora.scheduler.cron.CronException;
+import org.apache.aurora.scheduler.cron.CronJobManager;
 import org.apache.aurora.scheduler.cron.CronPredictor;
+import org.apache.aurora.scheduler.cron.CrontabEntry;
+import org.apache.aurora.scheduler.cron.SanitizedCronJob;
 import org.apache.aurora.scheduler.quota.QuotaInfo;
 import org.apache.aurora.scheduler.quota.QuotaManager;
-import org.apache.aurora.scheduler.state.CronJobManager;
 import org.apache.aurora.scheduler.state.LockManager;
 import org.apache.aurora.scheduler.state.LockManager.LockException;
 import org.apache.aurora.scheduler.state.MaintenanceController;
@@ -137,7 +140,7 @@ public class SchedulerThriftInterfaceTest extends EasyMockTest {
   private static final IJobKey JOB_KEY = JobKeys.from(ROLE, DEFAULT_ENVIRONMENT, JOB_NAME);
   private static final ILockKey LOCK_KEY = ILockKey.build(LockKey.job(JOB_KEY.newBuilder()));
   private static final ILock LOCK = ILock.build(new Lock().setKey(LOCK_KEY.newBuilder()));
-  private static final JobConfiguration CRON_JOB = makeJob().setCronSchedule("test");
+  private static final JobConfiguration CRON_JOB = makeJob().setCronSchedule("* * * * *");
   private static final Lock DEFAULT_LOCK = null;
 
   private static final IResourceAggregate QUOTA =
@@ -678,7 +681,7 @@ public class SchedulerThriftInterfaceTest extends EasyMockTest {
   public void testReplaceCronTemplate() throws Exception {
     expectAuth(ROLE, true);
     lockManager.validateIfLocked(LOCK_KEY, Optional.<ILock>absent());
-    cronJobManager.updateJob(anyObject(SanitizedConfiguration.class));
+    cronJobManager.updateJob(anyObject(SanitizedCronJob.class));
     control.replay();
 
     assertOkResponse(thrift.replaceCronTemplate(CRON_JOB, DEFAULT_LOCK, SESSION));
@@ -707,9 +710,9 @@ public class SchedulerThriftInterfaceTest extends EasyMockTest {
   public void testReplaceCronTemplateDoesNotExist() throws Exception {
     expectAuth(ROLE, true);
     lockManager.validateIfLocked(LOCK_KEY, Optional.<ILock>absent());
-    cronJobManager.updateJob(
-        SanitizedConfiguration.fromUnsanitized(IJobConfiguration.build(CRON_JOB)));
-    expectLastCall().andThrow(new ScheduleException("Nope"));
+    cronJobManager.updateJob(anyObject(SanitizedCronJob.class));
+    expectLastCall().andThrow(new CronException("Nope"));
+
     control.replay();
 
     assertResponse(INVALID_REQUEST, thrift.replaceCronTemplate(CRON_JOB, DEFAULT_LOCK, SESSION));
@@ -1004,7 +1007,7 @@ public class SchedulerThriftInterfaceTest extends EasyMockTest {
     Set<JobSummary> ownedImmedieteJobSummaryOnly = ImmutableSet.of(
         new JobSummary().setJob(ownedImmediateJob).setStats(new JobStats().setActiveTaskCount(1)));
 
-    expect(cronPredictor.predictNextRun(CRON_SCHEDULE))
+    expect(cronPredictor.predictNextRun(CrontabEntry.parse(CRON_SCHEDULE)))
         .andReturn(new Date(nextCronRunMs))
         .anyTimes();
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java b/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
index 488a545..5f32f21 100644
--- a/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
+++ b/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
@@ -34,8 +34,8 @@ import org.apache.aurora.gen.AuroraAdmin;
 import org.apache.aurora.gen.ResourceAggregate;
 import org.apache.aurora.gen.ServerInfo;
 import org.apache.aurora.gen.SessionKey;
+import org.apache.aurora.scheduler.cron.CronJobManager;
 import org.apache.aurora.scheduler.cron.CronPredictor;
-import org.apache.aurora.scheduler.cron.CronScheduler;
 import org.apache.aurora.scheduler.quota.QuotaManager;
 import org.apache.aurora.scheduler.state.LockManager;
 import org.apache.aurora.scheduler.state.MaintenanceController;
@@ -149,7 +149,7 @@ public class ThriftIT extends EasyMockTest {
 
           @Override
           protected void configure() {
-            bindMock(CronScheduler.class);
+            bindMock(CronJobManager.class);
             bindMock(MaintenanceController.class);
             bindMock(Recovery.class);
             bindMock(SchedulerCore.class);


[3/5] CronScheduler based on Quartz

Posted by ke...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/main/resources/org/apache/aurora/scheduler/cron/testing/cron-schedule-predictions.json
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/cron/testing/cron-schedule-predictions.json b/src/main/resources/org/apache/aurora/scheduler/cron/testing/cron-schedule-predictions.json
deleted file mode 100644
index dced8b4..0000000
--- a/src/main/resources/org/apache/aurora/scheduler/cron/testing/cron-schedule-predictions.json
+++ /dev/null
@@ -1,3332 +0,0 @@
-[
-    {
-        "schedule": "0 20 * * 1",
-        "triggerTimes": [
-            417600000,
-            1022400000,
-            1627200000,
-            2232000000,
-            2836800000,
-            3441600000,
-            4046400000,
-            4651200000,
-            5256000000,
-            5860800000
-        ]
-    },
-    {
-        "schedule": "11    *   *   *   *",
-        "triggerTimes": [
-            660000,
-            4260000,
-            7860000,
-            11460000,
-            15060000,
-            18660000,
-            22260000,
-            25860000,
-            29460000,
-            33060000
-        ]
-    },
-    {
-        "schedule": "04 02 * * *",
-        "triggerTimes": [
-            7440000,
-            93840000,
-            180240000,
-            266640000,
-            353040000,
-            439440000,
-            525840000,
-            612240000,
-            698640000,
-            785040000
-        ]
-    },
-    {
-        "schedule": "09 22 * * *",
-        "triggerTimes": [
-            79740000,
-            166140000,
-            252540000,
-            338940000,
-            425340000,
-            511740000,
-            598140000,
-            684540000,
-            770940000,
-            857340000
-        ]
-    },
-    {
-        "schedule": "1-56/5 * * * *",
-        "triggerTimes": [
-            60000,
-            360000,
-            660000,
-            960000,
-            1260000,
-            1560000,
-            1860000,
-            2160000,
-            2460000,
-            2760000
-        ]
-    },
-    {
-        "schedule": "05 02,08,12 * * *",
-        "triggerTimes": [
-            7500000,
-            29100000,
-            43500000,
-            93900000,
-            115500000,
-            129900000,
-            180300000,
-            201900000,
-            216300000,
-            266700000
-        ]
-    },
-    {
-        "schedule": "26 * * * *",
-        "triggerTimes": [
-            1560000,
-            5160000,
-            8760000,
-            12360000,
-            15960000,
-            19560000,
-            23160000,
-            26760000,
-            30360000,
-            33960000
-        ]
-    },
-    {
-        "schedule": "3,43 * * * *",
-        "triggerTimes": [
-            180000,
-            2580000,
-            3780000,
-            6180000,
-            7380000,
-            9780000,
-            10980000,
-            13380000,
-            14580000,
-            16980000
-        ]
-    },
-    {
-        "schedule": "0 17-23 * * 1-5",
-        "triggerTimes": [
-            61200000,
-            64800000,
-            68400000,
-            72000000,
-            75600000,
-            79200000,
-            82800000,
-            147600000,
-            151200000,
-            154800000
-        ]
-    },
-    {
-        "schedule": "0 0,12 * * *",
-        "triggerTimes": [
-            43200000,
-            86400000,
-            129600000,
-            172800000,
-            216000000,
-            259200000,
-            302400000,
-            345600000,
-            388800000,
-            432000000
-        ]
-    },
-    {
-        "schedule": "10 02,08,12 * * *",
-        "triggerTimes": [
-            7800000,
-            29400000,
-            43800000,
-            94200000,
-            115800000,
-            130200000,
-            180600000,
-            202200000,
-            216600000,
-            267000000
-        ]
-    },
-    {
-        "schedule": "50 */4 * * *",
-        "triggerTimes": [
-            3000000,
-            17400000,
-            31800000,
-            46200000,
-            60600000,
-            75000000,
-            89400000,
-            103800000,
-            118200000,
-            132600000
-        ]
-    },
-    {
-        "schedule": "10 02,08,14,20 * * *",
-        "triggerTimes": [
-            7800000,
-            29400000,
-            51000000,
-            72600000,
-            94200000,
-            115800000,
-            137400000,
-            159000000,
-            180600000,
-            202200000
-        ]
-    },
-    {
-        "schedule": "0 */6 * * *",
-        "triggerTimes": [
-            21600000,
-            43200000,
-            64800000,
-            86400000,
-            108000000,
-            129600000,
-            151200000,
-            172800000,
-            194400000,
-            216000000
-        ]
-    },
-    {
-        "schedule": "* * * * *",
-        "triggerTimes": [
-            60000,
-            120000,
-            180000,
-            240000,
-            300000,
-            360000,
-            420000,
-            480000,
-            540000,
-            600000
-        ]
-    },
-    {
-        "schedule": "30 15 * * *,",
-        "triggerTimes": [
-            55800000,
-            142200000,
-            228600000,
-            315000000,
-            401400000,
-            487800000,
-            574200000,
-            660600000,
-            747000000,
-            833400000
-        ]
-    },
-    {
-        "schedule": "00 11 * * *",
-        "triggerTimes": [
-            39600000,
-            126000000,
-            212400000,
-            298800000,
-            385200000,
-            471600000,
-            558000000,
-            644400000,
-            730800000,
-            817200000
-        ]
-    },
-    {
-        "schedule": "55 06 * * *",
-        "triggerTimes": [
-            24900000,
-            111300000,
-            197700000,
-            284100000,
-            370500000,
-            456900000,
-            543300000,
-            629700000,
-            716100000,
-            802500000
-        ]
-    },
-    {
-        "schedule": "0 4 * * *",
-        "triggerTimes": [
-            14400000,
-            100800000,
-            187200000,
-            273600000,
-            360000000,
-            446400000,
-            532800000,
-            619200000,
-            705600000,
-            792000000
-        ]
-    },
-    {
-        "schedule": "55 */1 * * *",
-        "triggerTimes": [
-            3300000,
-            6900000,
-            10500000,
-            14100000,
-            17700000,
-            21300000,
-            24900000,
-            28500000,
-            32100000,
-            35700000
-        ]
-    },
-    {
-        "schedule": "15 */3 * * *",
-        "triggerTimes": [
-            900000,
-            11700000,
-            22500000,
-            33300000,
-            44100000,
-            54900000,
-            65700000,
-            76500000,
-            87300000,
-            98100000
-        ]
-    },
-    {
-        "schedule": "42 8,12,16 * * *",
-        "triggerTimes": [
-            31320000,
-            45720000,
-            60120000,
-            117720000,
-            132120000,
-            146520000,
-            204120000,
-            218520000,
-            232920000,
-            290520000
-        ]
-    },
-    {
-        "schedule": "23 * * * *",
-        "triggerTimes": [
-            1380000,
-            4980000,
-            8580000,
-            12180000,
-            15780000,
-            19380000,
-            22980000,
-            26580000,
-            30180000,
-            33780000
-        ]
-    },
-    {
-        "schedule": "10 16 * * *",
-        "triggerTimes": [
-            58200000,
-            144600000,
-            231000000,
-            317400000,
-            403800000,
-            490200000,
-            576600000,
-            663000000,
-            749400000,
-            835800000
-        ]
-    },
-    {
-        "schedule": "*/30 * * * *",
-        "triggerTimes": [
-            1800000,
-            3600000,
-            5400000,
-            7200000,
-            9000000,
-            10800000,
-            12600000,
-            14400000,
-            16200000,
-            18000000
-        ]
-    },
-    {
-        "schedule": "20 */3 * * *",
-        "triggerTimes": [
-            1200000,
-            12000000,
-            22800000,
-            33600000,
-            44400000,
-            55200000,
-            66000000,
-            76800000,
-            87600000,
-            98400000
-        ]
-    },
-    {
-        "schedule": "8 6,12,18 * * *",
-        "triggerTimes": [
-            22080000,
-            43680000,
-            65280000,
-            108480000,
-            130080000,
-            151680000,
-            194880000,
-            216480000,
-            238080000,
-            281280000
-        ]
-    },
-    {
-        "schedule": "30 7,12,22 * * *",
-        "triggerTimes": [
-            27000000,
-            45000000,
-            81000000,
-            113400000,
-            131400000,
-            167400000,
-            199800000,
-            217800000,
-            253800000,
-            286200000
-        ]
-    },
-    {
-        "schedule": "0 0 12 * *",
-        "triggerTimes": [
-            950400000,
-            3628800000,
-            6048000000,
-            8726400000,
-            11318400000,
-            13996800000,
-            16588800000,
-            19267200000,
-            21945600000,
-            24537600000
-        ]
-    },
-    {
-        "schedule": "17 5,8,13,16,19 * * *",
-        "triggerTimes": [
-            19020000,
-            29820000,
-            47820000,
-            58620000,
-            69420000,
-            105420000,
-            116220000,
-            134220000,
-            145020000,
-            155820000
-        ]
-    },
-    {
-        "schedule": "27 8,20 * * *",
-        "triggerTimes": [
-            30420000,
-            73620000,
-            116820000,
-            160020000,
-            203220000,
-            246420000,
-            289620000,
-            332820000,
-            376020000,
-            419220000
-        ]
-    },
-    {
-        "schedule": "15 */6 * * *",
-        "triggerTimes": [
-            900000,
-            22500000,
-            44100000,
-            65700000,
-            87300000,
-            108900000,
-            130500000,
-            152100000,
-            173700000,
-            195300000
-        ]
-    },
-    {
-        "schedule": "01 15 * * *",
-        "triggerTimes": [
-            54060000,
-            140460000,
-            226860000,
-            313260000,
-            399660000,
-            486060000,
-            572460000,
-            658860000,
-            745260000,
-            831660000
-        ]
-    },
-    {
-        "schedule": "0 18 * * *",
-        "triggerTimes": [
-            64800000,
-            151200000,
-            237600000,
-            324000000,
-            410400000,
-            496800000,
-            583200000,
-            669600000,
-            756000000,
-            842400000
-        ]
-    },
-    {
-        "schedule": "24 * * * *",
-        "triggerTimes": [
-            1440000,
-            5040000,
-            8640000,
-            12240000,
-            15840000,
-            19440000,
-            23040000,
-            26640000,
-            30240000,
-            33840000
-        ]
-    },
-    {
-        "schedule": "18 00 * * *",
-        "triggerTimes": [
-            1080000,
-            87480000,
-            173880000,
-            260280000,
-            346680000,
-            433080000,
-            519480000,
-            605880000,
-            692280000,
-            778680000
-        ]
-    },
-    {
-        "schedule": "0 16 * * *",
-        "triggerTimes": [
-            57600000,
-            144000000,
-            230400000,
-            316800000,
-            403200000,
-            489600000,
-            576000000,
-            662400000,
-            748800000,
-            835200000
-        ]
-    },
-    {
-        "schedule": "45 5 * * *",
-        "triggerTimes": [
-            20700000,
-            107100000,
-            193500000,
-            279900000,
-            366300000,
-            452700000,
-            539100000,
-            625500000,
-            711900000,
-            798300000
-        ]
-    },
-    {
-        "schedule": "0 18 * * 4",
-        "triggerTimes": [
-            64800000,
-            669600000,
-            1274400000,
-            1879200000,
-            2484000000,
-            3088800000,
-            3693600000,
-            4298400000,
-            4903200000,
-            5508000000
-        ]
-    },
-    {
-        "schedule": "30 19 * * *",
-        "triggerTimes": [
-            70200000,
-            156600000,
-            243000000,
-            329400000,
-            415800000,
-            502200000,
-            588600000,
-            675000000,
-            761400000,
-            847800000
-        ]
-    },
-    {
-        "schedule": "0 13 * * 2",
-        "triggerTimes": [
-            478800000,
-            1083600000,
-            1688400000,
-            2293200000,
-            2898000000,
-            3502800000,
-            4107600000,
-            4712400000,
-            5317200000,
-            5922000000
-        ]
-    },
-    {
-        "schedule": "25 17,20,21,23 * * *",
-        "triggerTimes": [
-            62700000,
-            73500000,
-            77100000,
-            84300000,
-            149100000,
-            159900000,
-            163500000,
-            170700000,
-            235500000,
-            246300000
-        ]
-    },
-    {
-        "schedule": "0 13 * * 3",
-        "triggerTimes": [
-            565200000,
-            1170000000,
-            1774800000,
-            2379600000,
-            2984400000,
-            3589200000,
-            4194000000,
-            4798800000,
-            5403600000,
-            6008400000
-        ]
-    },
-    {
-        "schedule": "58 */2 * * *",
-        "triggerTimes": [
-            3480000,
-            10680000,
-            17880000,
-            25080000,
-            32280000,
-            39480000,
-            46680000,
-            53880000,
-            61080000,
-            68280000
-        ]
-    },
-    {
-        "schedule": "0 9 4,18 * *",
-        "triggerTimes": [
-            291600000,
-            1501200000,
-            2970000000,
-            4179600000,
-            5389200000,
-            6598800000,
-            8067600000,
-            9277200000,
-            10659600000,
-            11869200000
-        ]
-    },
-    {
-        "schedule": "37    */6 *   *   *",
-        "triggerTimes": [
-            2220000,
-            23820000,
-            45420000,
-            67020000,
-            88620000,
-            110220000,
-            131820000,
-            153420000,
-            175020000,
-            196620000
-        ]
-    },
-    {
-        "schedule": "00 14 * * *",
-        "triggerTimes": [
-            50400000,
-            136800000,
-            223200000,
-            309600000,
-            396000000,
-            482400000,
-            568800000,
-            655200000,
-            741600000,
-            828000000
-        ]
-    },
-    {
-        "schedule": "0 * * * *",
-        "triggerTimes": [
-            3600000,
-            7200000,
-            10800000,
-            14400000,
-            18000000,
-            21600000,
-            25200000,
-            28800000,
-            32400000,
-            36000000
-        ]
-    },
-    {
-        "schedule": "29 9,16,22 * * *",
-        "triggerTimes": [
-            34140000,
-            59340000,
-            80940000,
-            120540000,
-            145740000,
-            167340000,
-            206940000,
-            232140000,
-            253740000,
-            293340000
-        ]
-    },
-    {
-        "schedule": "37 3 * * *",
-        "triggerTimes": [
-            13020000,
-            99420000,
-            185820000,
-            272220000,
-            358620000,
-            445020000,
-            531420000,
-            617820000,
-            704220000,
-            790620000
-        ]
-    },
-    {
-        "schedule": "*/5 * * * *",
-        "triggerTimes": [
-            300000,
-            600000,
-            900000,
-            1200000,
-            1500000,
-            1800000,
-            2100000,
-            2400000,
-            2700000,
-            3000000
-        ]
-    },
-    {
-        "schedule": "7 */2 * * *",
-        "triggerTimes": [
-            420000,
-            7620000,
-            14820000,
-            22020000,
-            29220000,
-            36420000,
-            43620000,
-            50820000,
-            58020000,
-            65220000
-        ]
-    },
-    {
-        "schedule": "55 07 * * *",
-        "triggerTimes": [
-            28500000,
-            114900000,
-            201300000,
-            287700000,
-            374100000,
-            460500000,
-            546900000,
-            633300000,
-            719700000,
-            806100000
-        ]
-    },
-    {
-        "schedule": "0 19 * * *",
-        "triggerTimes": [
-            68400000,
-            154800000,
-            241200000,
-            327600000,
-            414000000,
-            500400000,
-            586800000,
-            673200000,
-            759600000,
-            846000000
-        ]
-    },
-    {
-        "schedule": "15 */2 * * *",
-        "triggerTimes": [
-            900000,
-            8100000,
-            15300000,
-            22500000,
-            29700000,
-            36900000,
-            44100000,
-            51300000,
-            58500000,
-            65700000
-        ]
-    },
-    {
-        "schedule": "17 00 * * *",
-        "triggerTimes": [
-            1020000,
-            87420000,
-            173820000,
-            260220000,
-            346620000,
-            433020000,
-            519420000,
-            605820000,
-            692220000,
-            778620000
-        ]
-    },
-    {
-        "schedule": "0 0 * * 1",
-        "triggerTimes": [
-            345600000,
-            950400000,
-            1555200000,
-            2160000000,
-            2764800000,
-            3369600000,
-            3974400000,
-            4579200000,
-            5184000000,
-            5788800000
-        ]
-    },
-    {
-        "schedule": "29 */4 * * *",
-        "triggerTimes": [
-            1740000,
-            16140000,
-            30540000,
-            44940000,
-            59340000,
-            73740000,
-            88140000,
-            102540000,
-            116940000,
-            131340000
-        ]
-    },
-    {
-        "schedule": "0 23 * * *",
-        "triggerTimes": [
-            82800000,
-            169200000,
-            255600000,
-            342000000,
-            428400000,
-            514800000,
-            601200000,
-            687600000,
-            774000000,
-            860400000
-        ]
-    },
-    {
-        "schedule": "0 7 * * *",
-        "triggerTimes": [
-            25200000,
-            111600000,
-            198000000,
-            284400000,
-            370800000,
-            457200000,
-            543600000,
-            630000000,
-            716400000,
-            802800000
-        ]
-    },
-    {
-        "schedule": "12 * * * *",
-        "triggerTimes": [
-            720000,
-            4320000,
-            7920000,
-            11520000,
-            15120000,
-            18720000,
-            22320000,
-            25920000,
-            29520000,
-            33120000
-        ]
-    },
-    {
-        "schedule": "0 23 * * 3",
-        "triggerTimes": [
-            601200000,
-            1206000000,
-            1810800000,
-            2415600000,
-            3020400000,
-            3625200000,
-            4230000000,
-            4834800000,
-            5439600000,
-            6044400000
-        ]
-    },
-    {
-        "schedule": "23 */4 * * *",
-        "triggerTimes": [
-            1380000,
-            15780000,
-            30180000,
-            44580000,
-            58980000,
-            73380000,
-            87780000,
-            102180000,
-            116580000,
-            130980000
-        ]
-    },
-    {
-        "schedule": "30 1-23/2 * * *",
-        "triggerTimes": [
-            5400000,
-            12600000,
-            19800000,
-            27000000,
-            34200000,
-            41400000,
-            48600000,
-            55800000,
-            63000000,
-            70200000
-        ]
-    },
-    {
-        "schedule": "5,15,25,35,45,55 * * * *",
-        "triggerTimes": [
-            300000,
-            900000,
-            1500000,
-            2100000,
-            2700000,
-            3300000,
-            3900000,
-            4500000,
-            5100000,
-            5700000
-        ]
-    },
-    {
-        "schedule": "23 1,11,21 * * *",
-        "triggerTimes": [
-            4980000,
-            40980000,
-            76980000,
-            91380000,
-            127380000,
-            163380000,
-            177780000,
-            213780000,
-            249780000,
-            264180000
-        ]
-    },
-    {
-        "schedule": "15 04,10,16,22 * * *",
-        "triggerTimes": [
-            15300000,
-            36900000,
-            58500000,
-            80100000,
-            101700000,
-            123300000,
-            144900000,
-            166500000,
-            188100000,
-            209700000
-        ]
-    },
-    {
-        "schedule": "*/20  *   *   *   *",
-        "triggerTimes": [
-            1200000,
-            2400000,
-            3600000,
-            4800000,
-            6000000,
-            7200000,
-            8400000,
-            9600000,
-            10800000,
-            12000000
-        ]
-    },
-    {
-        "schedule": "12,42 * * * *",
-        "triggerTimes": [
-            720000,
-            2520000,
-            4320000,
-            6120000,
-            7920000,
-            9720000,
-            11520000,
-            13320000,
-            15120000,
-            16920000
-        ]
-    },
-    {
-        "schedule": "26 2,6,10,14,18,22 * * *",
-        "triggerTimes": [
-            8760000,
-            23160000,
-            37560000,
-            51960000,
-            66360000,
-            80760000,
-            95160000,
-            109560000,
-            123960000,
-            138360000
-        ]
-    },
-    {
-        "schedule": "0 3,6,9,12,15,18,21 * * *",
-        "triggerTimes": [
-            10800000,
-            21600000,
-            32400000,
-            43200000,
-            54000000,
-            64800000,
-            75600000,
-            97200000,
-            108000000,
-            118800000
-        ]
-    },
-    {
-        "schedule": "25 14 * * *",
-        "triggerTimes": [
-            51900000,
-            138300000,
-            224700000,
-            311100000,
-            397500000,
-            483900000,
-            570300000,
-            656700000,
-            743100000,
-            829500000
-        ]
-    },
-    {
-        "schedule": "0 5 * * *,",
-        "triggerTimes": [
-            18000000,
-            104400000,
-            190800000,
-            277200000,
-            363600000,
-            450000000,
-            536400000,
-            622800000,
-            709200000,
-            795600000
-        ]
-    },
-    {
-        "schedule": "43 * * * *",
-        "triggerTimes": [
-            2580000,
-            6180000,
-            9780000,
-            13380000,
-            16980000,
-            20580000,
-            24180000,
-            27780000,
-            31380000,
-            34980000
-        ]
-    },
-    {
-        "schedule": "39 6,12,16 * * *",
-        "triggerTimes": [
-            23940000,
-            45540000,
-            59940000,
-            110340000,
-            131940000,
-            146340000,
-            196740000,
-            218340000,
-            232740000,
-            283140000
-        ]
-    },
-    {
-        "schedule": "0 9 1 * *",
-        "triggerTimes": [
-            32400000,
-            2710800000,
-            5130000000,
-            7808400000,
-            10400400000,
-            13078800000,
-            15670800000,
-            18349200000,
-            21027600000,
-            23619600000
-        ]
-    },
-    {
-        "schedule": "14-59/30 * * * *",
-        "triggerTimes": [
-            840000,
-            2640000,
-            4440000,
-            6240000,
-            8040000,
-            9840000,
-            11640000,
-            13440000,
-            15240000,
-            17040000
-        ]
-    },
-    {
-        "schedule": "0 0 * * *",
-        "triggerTimes": [
-            86400000,
-            172800000,
-            259200000,
-            345600000,
-            432000000,
-            518400000,
-            604800000,
-            691200000,
-            777600000,
-            864000000
-        ]
-    },
-    {
-        "schedule": "0 */3 * * *",
-        "triggerTimes": [
-            10800000,
-            21600000,
-            32400000,
-            43200000,
-            54000000,
-            64800000,
-            75600000,
-            86400000,
-            97200000,
-            108000000
-        ]
-    },
-    {
-        "schedule": "16 5,13,21 * * *",
-        "triggerTimes": [
-            18960000,
-            47760000,
-            76560000,
-            105360000,
-            134160000,
-            162960000,
-            191760000,
-            220560000,
-            249360000,
-            278160000
-        ]
-    },
-    {
-        "schedule": "30 18,23 * * MON-FRI",
-        "triggerTimes": [
-            66600000,
-            84600000,
-            153000000,
-            171000000,
-            412200000,
-            430200000,
-            498600000,
-            516600000,
-            585000000,
-            603000000
-        ]
-    },
-    {
-        "schedule": "0,15,30,45 * * * *",
-        "triggerTimes": [
-            900000,
-            1800000,
-            2700000,
-            3600000,
-            4500000,
-            5400000,
-            6300000,
-            7200000,
-            8100000,
-            9000000
-        ]
-    },
-    {
-        "schedule": "42 8,20 * * *",
-        "triggerTimes": [
-            31320000,
-            74520000,
-            117720000,
-            160920000,
-            204120000,
-            247320000,
-            290520000,
-            333720000,
-            376920000,
-            420120000
-        ]
-    },
-    {
-        "schedule": "46 */6 * * *",
-        "triggerTimes": [
-            2760000,
-            24360000,
-            45960000,
-            67560000,
-            89160000,
-            110760000,
-            132360000,
-            153960000,
-            175560000,
-            197160000
-        ]
-    },
-    {
-        "schedule": "0 3 * * *",
-        "triggerTimes": [
-            10800000,
-            97200000,
-            183600000,
-            270000000,
-            356400000,
-            442800000,
-            529200000,
-            615600000,
-            702000000,
-            788400000
-        ]
-    },
-    {
-        "schedule": "16 9,16 * * *",
-        "triggerTimes": [
-            33360000,
-            58560000,
-            119760000,
-            144960000,
-            206160000,
-            231360000,
-            292560000,
-            317760000,
-            378960000,
-            404160000
-        ]
-    },
-    {
-        "schedule": "15 0 * * *",
-        "triggerTimes": [
-            900000,
-            87300000,
-            173700000,
-            260100000,
-            346500000,
-            432900000,
-            519300000,
-            605700000,
-            692100000,
-            778500000
-        ]
-    },
-    {
-        "schedule": "05 * * * *",
-        "triggerTimes": [
-            300000,
-            3900000,
-            7500000,
-            11100000,
-            14700000,
-            18300000,
-            21900000,
-            25500000,
-            29100000,
-            32700000
-        ]
-    },
-    {
-        "schedule": "30 * * * *",
-        "triggerTimes": [
-            1800000,
-            5400000,
-            9000000,
-            12600000,
-            16200000,
-            19800000,
-            23400000,
-            27000000,
-            30600000,
-            34200000
-        ]
-    },
-    {
-        "schedule": "0 2,14 * * *",
-        "triggerTimes": [
-            7200000,
-            50400000,
-            93600000,
-            136800000,
-            180000000,
-            223200000,
-            266400000,
-            309600000,
-            352800000,
-            396000000
-        ]
-    },
-    {
-        "schedule": "28 23 * * 3",
-        "triggerTimes": [
-            602880000,
-            1207680000,
-            1812480000,
-            2417280000,
-            3022080000,
-            3626880000,
-            4231680000,
-            4836480000,
-            5441280000,
-            6046080000
-        ]
-    },
-    {
-        "schedule": "5 */4 * * *",
-        "triggerTimes": [
-            300000,
-            14700000,
-            29100000,
-            43500000,
-            57900000,
-            72300000,
-            86700000,
-            101100000,
-            115500000,
-            129900000
-        ]
-    },
-    {
-        "schedule": "0 18,22 * * MON-FRI",
-        "triggerTimes": [
-            64800000,
-            79200000,
-            151200000,
-            165600000,
-            410400000,
-            424800000,
-            496800000,
-            511200000,
-            583200000,
-            597600000
-        ]
-    },
-    {
-        "schedule": "01 21 * * *",
-        "triggerTimes": [
-            75660000,
-            162060000,
-            248460000,
-            334860000,
-            421260000,
-            507660000,
-            594060000,
-            680460000,
-            766860000,
-            853260000
-        ]
-    },
-    {
-        "schedule": "1 */6 * * *",
-        "triggerTimes": [
-            60000,
-            21660000,
-            43260000,
-            64860000,
-            86460000,
-            108060000,
-            129660000,
-            151260000,
-            172860000,
-            194460000
-        ]
-    },
-    {
-        "schedule": "*/10 * * * *",
-        "triggerTimes": [
-            600000,
-            1200000,
-            1800000,
-            2400000,
-            3000000,
-            3600000,
-            4200000,
-            4800000,
-            5400000,
-            6000000
-        ]
-    },
-    {
-        "schedule": "44    */2 *   *   *",
-        "triggerTimes": [
-            2640000,
-            9840000,
-            17040000,
-            24240000,
-            31440000,
-            38640000,
-            45840000,
-            53040000,
-            60240000,
-            67440000
-        ]
-    },
-    {
-        "schedule": "30 2 * * *",
-        "triggerTimes": [
-            9000000,
-            95400000,
-            181800000,
-            268200000,
-            354600000,
-            441000000,
-            527400000,
-            613800000,
-            700200000,
-            786600000
-        ]
-    },
-    {
-        "schedule": "58 * * * *",
-        "triggerTimes": [
-            3480000,
-            7080000,
-            10680000,
-            14280000,
-            17880000,
-            21480000,
-            25080000,
-            28680000,
-            32280000,
-            35880000
-        ]
-    },
-    {
-        "schedule": "30 23 * * 6",
-        "triggerTimes": [
-            257400000,
-            862200000,
-            1467000000,
-            2071800000,
-            2676600000,
-            3281400000,
-            3886200000,
-            4491000000,
-            5095800000,
-            5700600000
-        ]
-    },
-    {
-        "schedule": "40 23 * * *",
-        "triggerTimes": [
-            85200000,
-            171600000,
-            258000000,
-            344400000,
-            430800000,
-            517200000,
-            603600000,
-            690000000,
-            776400000,
-            862800000
-        ]
-    },
-    {
-        "schedule": "0 5,10,15,20,1 * * *",
-        "triggerTimes": [
-            3600000,
-            18000000,
-            36000000,
-            54000000,
-            72000000,
-            90000000,
-            104400000,
-            122400000,
-            140400000,
-            158400000
-        ]
-    },
-    {
-        "schedule": "22 * * * *",
-        "triggerTimes": [
-            1320000,
-            4920000,
-            8520000,
-            12120000,
-            15720000,
-            19320000,
-            22920000,
-            26520000,
-            30120000,
-            33720000
-        ]
-    },
-    {
-        "schedule": "00 17 1-3,5-31 * *",
-        "triggerTimes": [
-            61200000,
-            147600000,
-            234000000,
-            406800000,
-            493200000,
-            579600000,
-            666000000,
-            752400000,
-            838800000,
-            925200000
-        ]
-    },
-    {
-        "schedule": "0 2 1 * *",
-        "triggerTimes": [
-            7200000,
-            2685600000,
-            5104800000,
-            7783200000,
-            10375200000,
-            13053600000,
-            15645600000,
-            18324000000,
-            21002400000,
-            23594400000
-        ]
-    },
-    {
-        "schedule": "20 20 * * *",
-        "triggerTimes": [
-            73200000,
-            159600000,
-            246000000,
-            332400000,
-            418800000,
-            505200000,
-            591600000,
-            678000000,
-            764400000,
-            850800000
-        ]
-    },
-    {
-        "schedule": "45 1 * * *",
-        "triggerTimes": [
-            6300000,
-            92700000,
-            179100000,
-            265500000,
-            351900000,
-            438300000,
-            524700000,
-            611100000,
-            697500000,
-            783900000
-        ]
-    },
-    {
-        "schedule": "3-59/5 * * * *",
-        "triggerTimes": [
-            180000,
-            480000,
-            780000,
-            1080000,
-            1380000,
-            1680000,
-            1980000,
-            2280000,
-            2580000,
-            2880000
-        ]
-    },
-    {
-        "schedule": "21    *   *   *   *",
-        "triggerTimes": [
-            1260000,
-            4860000,
-            8460000,
-            12060000,
-            15660000,
-            19260000,
-            22860000,
-            26460000,
-            30060000,
-            33660000
-        ]
-    },
-    {
-        "schedule": "37 */1 * * *",
-        "triggerTimes": [
-            2220000,
-            5820000,
-            9420000,
-            13020000,
-            16620000,
-            20220000,
-            23820000,
-            27420000,
-            31020000,
-            34620000
-        ]
-    },
-    {
-        "schedule": "12 3 * * 1,3,5",
-        "triggerTimes": [
-            97920000,
-            357120000,
-            529920000,
-            702720000,
-            961920000,
-            1134720000,
-            1307520000,
-            1566720000,
-            1739520000,
-            1912320000
-        ]
-    },
-    {
-        "schedule": "10 * * * *",
-        "triggerTimes": [
-            600000,
-            4200000,
-            7800000,
-            11400000,
-            15000000,
-            18600000,
-            22200000,
-            25800000,
-            29400000,
-            33000000
-        ]
-    },
-    {
-        "schedule": "*/4 * * * *",
-        "triggerTimes": [
-            240000,
-            480000,
-            720000,
-            960000,
-            1200000,
-            1440000,
-            1680000,
-            1920000,
-            2160000,
-            2400000
-        ]
-    },
-    {
-        "schedule": "36 * * * *",
-        "triggerTimes": [
-            2160000,
-            5760000,
-            9360000,
-            12960000,
-            16560000,
-            20160000,
-            23760000,
-            27360000,
-            30960000,
-            34560000
-        ]
-    },
-    {
-        "schedule": "10 7 * * *",
-        "triggerTimes": [
-            25800000,
-            112200000,
-            198600000,
-            285000000,
-            371400000,
-            457800000,
-            544200000,
-            630600000,
-            717000000,
-            803400000
-        ]
-    },
-    {
-        "schedule": "55 6 * * *",
-        "triggerTimes": [
-            24900000,
-            111300000,
-            197700000,
-            284100000,
-            370500000,
-            456900000,
-            543300000,
-            629700000,
-            716100000,
-            802500000
-        ]
-    },
-    {
-        "schedule": "0 */2 * * *",
-        "triggerTimes": [
-            7200000,
-            14400000,
-            21600000,
-            28800000,
-            36000000,
-            43200000,
-            50400000,
-            57600000,
-            64800000,
-            72000000
-        ]
-    },
-    {
-        "schedule": "0 5 * * *",
-        "triggerTimes": [
-            18000000,
-            104400000,
-            190800000,
-            277200000,
-            363600000,
-            450000000,
-            536400000,
-            622800000,
-            709200000,
-            795600000
-        ]
-    },
-    {
-        "schedule": "22 */4 * * *",
-        "triggerTimes": [
-            1320000,
-            15720000,
-            30120000,
-            44520000,
-            58920000,
-            73320000,
-            87720000,
-            102120000,
-            116520000,
-            130920000
-        ]
-    },
-    {
-        "schedule": "17 */2 * * *",
-        "triggerTimes": [
-            1020000,
-            8220000,
-            15420000,
-            22620000,
-            29820000,
-            37020000,
-            44220000,
-            51420000,
-            58620000,
-            65820000
-        ]
-    },
-    {
-        "schedule": "25    *   *   *   *",
-        "triggerTimes": [
-            1500000,
-            5100000,
-            8700000,
-            12300000,
-            15900000,
-            19500000,
-            23100000,
-            26700000,
-            30300000,
-            33900000
-        ]
-    },
-    {
-        "schedule": "*/6 * * * *",
-        "triggerTimes": [
-            360000,
-            720000,
-            1080000,
-            1440000,
-            1800000,
-            2160000,
-            2520000,
-            2880000,
-            3240000,
-            3600000
-        ]
-    },
-    {
-        "schedule": "5 * * * *",
-        "triggerTimes": [
-            300000,
-            3900000,
-            7500000,
-            11100000,
-            14700000,
-            18300000,
-            21900000,
-            25500000,
-            29100000,
-            32700000
-        ]
-    },
-    {
-        "schedule": "0 2 * * *",
-        "triggerTimes": [
-            7200000,
-            93600000,
-            180000000,
-            266400000,
-            352800000,
-            439200000,
-            525600000,
-            612000000,
-            698400000,
-            784800000
-        ]
-    },
-    {
-        "schedule": "0     *   *   *   *",
-        "triggerTimes": [
-            3600000,
-            7200000,
-            10800000,
-            14400000,
-            18000000,
-            21600000,
-            25200000,
-            28800000,
-            32400000,
-            36000000
-        ]
-    },
-    {
-        "schedule": "0 14 * * *,,",
-        "triggerTimes": [
-            50400000,
-            136800000,
-            223200000,
-            309600000,
-            396000000,
-            482400000,
-            568800000,
-            655200000,
-            741600000,
-            828000000
-        ]
-    },
-    {
-        "schedule": "30 02,08,12 * * *",
-        "triggerTimes": [
-            9000000,
-            30600000,
-            45000000,
-            95400000,
-            117000000,
-            131400000,
-            181800000,
-            203400000,
-            217800000,
-            268200000
-        ]
-    },
-    {
-        "schedule": "44 23 * * *",
-        "triggerTimes": [
-            85440000,
-            171840000,
-            258240000,
-            344640000,
-            431040000,
-            517440000,
-            603840000,
-            690240000,
-            776640000,
-            863040000
-        ]
-    },
-    {
-        "schedule": "0 */4 * * *",
-        "triggerTimes": [
-            14400000,
-            28800000,
-            43200000,
-            57600000,
-            72000000,
-            86400000,
-            100800000,
-            115200000,
-            129600000,
-            144000000
-        ]
-    },
-    {
-        "schedule": "0 12 * * *",
-        "triggerTimes": [
-            43200000,
-            129600000,
-            216000000,
-            302400000,
-            388800000,
-            475200000,
-            561600000,
-            648000000,
-            734400000,
-            820800000
-        ]
-    },
-    {
-        "schedule": "*/2   *   *   *   *",
-        "triggerTimes": [
-            120000,
-            240000,
-            360000,
-            480000,
-            600000,
-            720000,
-            840000,
-            960000,
-            1080000,
-            1200000
-        ]
-    },
-    {
-        "schedule": "22    1   *   *   *",
-        "triggerTimes": [
-            4920000,
-            91320000,
-            177720000,
-            264120000,
-            350520000,
-            436920000,
-            523320000,
-            609720000,
-            696120000,
-            782520000
-        ]
-    },
-    {
-        "schedule": "45 * * * *",
-        "triggerTimes": [
-            2700000,
-            6300000,
-            9900000,
-            13500000,
-            17100000,
-            20700000,
-            24300000,
-            27900000,
-            31500000,
-            35100000
-        ]
-    },
-    {
-        "schedule": "00 23 * * *",
-        "triggerTimes": [
-            82800000,
-            169200000,
-            255600000,
-            342000000,
-            428400000,
-            514800000,
-            601200000,
-            687600000,
-            774000000,
-            860400000
-        ]
-    },
-    {
-        "schedule": "3,6,9,12,18,21,24,27,33,36,39,42,48,51,54,57 * * * *",
-        "triggerTimes": [
-            180000,
-            360000,
-            540000,
-            720000,
-            1080000,
-            1260000,
-            1440000,
-            1620000,
-            1980000,
-            2160000
-        ]
-    },
-    {
-        "schedule": "32    1   *   *   *",
-        "triggerTimes": [
-            5520000,
-            91920000,
-            178320000,
-            264720000,
-            351120000,
-            437520000,
-            523920000,
-            610320000,
-            696720000,
-            783120000
-        ]
-    },
-    {
-        "schedule": "35 */2 * * *",
-        "triggerTimes": [
-            2100000,
-            9300000,
-            16500000,
-            23700000,
-            30900000,
-            38100000,
-            45300000,
-            52500000,
-            59700000,
-            66900000
-        ]
-    },
-    {
-        "schedule": "27    1   *   *   *",
-        "triggerTimes": [
-            5220000,
-            91620000,
-            178020000,
-            264420000,
-            350820000,
-            437220000,
-            523620000,
-            610020000,
-            696420000,
-            782820000
-        ]
-    },
-    {
-        "schedule": "0 21 * * 3",
-        "triggerTimes": [
-            594000000,
-            1198800000,
-            1803600000,
-            2408400000,
-            3013200000,
-            3618000000,
-            4222800000,
-            4827600000,
-            5432400000,
-            6037200000
-        ]
-    },
-    {
-        "schedule": "55 03 * * *",
-        "triggerTimes": [
-            14100000,
-            100500000,
-            186900000,
-            273300000,
-            359700000,
-            446100000,
-            532500000,
-            618900000,
-            705300000,
-            791700000
-        ]
-    },
-    {
-        "schedule": "0 23 2-31 * *",
-        "triggerTimes": [
-            169200000,
-            255600000,
-            342000000,
-            428400000,
-            514800000,
-            601200000,
-            687600000,
-            774000000,
-            860400000,
-            946800000
-        ]
-    },
-    {
-        "schedule": "09 11 * * *",
-        "triggerTimes": [
-            40140000,
-            126540000,
-            212940000,
-            299340000,
-            385740000,
-            472140000,
-            558540000,
-            644940000,
-            731340000,
-            817740000
-        ]
-    },
-    {
-        "schedule": "0 14 * * *",
-        "triggerTimes": [
-            50400000,
-            136800000,
-            223200000,
-            309600000,
-            396000000,
-            482400000,
-            568800000,
-            655200000,
-            741600000,
-            828000000
-        ]
-    },
-    {
-        "schedule": "20 2,12,22 * * *",
-        "triggerTimes": [
-            8400000,
-            44400000,
-            80400000,
-            94800000,
-            130800000,
-            166800000,
-            181200000,
-            217200000,
-            253200000,
-            267600000
-        ]
-    },
-    {
-        "schedule": "2,6,10,14,18,22,26,30,34,38,42,46,50,54,58 * * * *",
-        "triggerTimes": [
-            120000,
-            360000,
-            600000,
-            840000,
-            1080000,
-            1320000,
-            1560000,
-            1800000,
-            2040000,
-            2280000
-        ]
-    },
-    {
-        "schedule": "1 16,18,20 * * *",
-        "triggerTimes": [
-            57660000,
-            64860000,
-            72060000,
-            144060000,
-            151260000,
-            158460000,
-            230460000,
-            237660000,
-            244860000,
-            316860000
-        ]
-    },
-    {
-        "schedule": "30 */6 * * *",
-        "triggerTimes": [
-            1800000,
-            23400000,
-            45000000,
-            66600000,
-            88200000,
-            109800000,
-            131400000,
-            153000000,
-            174600000,
-            196200000
-        ]
-    },
-    {
-        "schedule": "00 06,15 * * *",
-        "triggerTimes": [
-            21600000,
-            54000000,
-            108000000,
-            140400000,
-            194400000,
-            226800000,
-            280800000,
-            313200000,
-            367200000,
-            399600000
-        ]
-    },
-    {
-        "schedule": "52 4,10,16,22 * * *",
-        "triggerTimes": [
-            17520000,
-            39120000,
-            60720000,
-            82320000,
-            103920000,
-            125520000,
-            147120000,
-            168720000,
-            190320000,
-            211920000
-        ]
-    },
-    {
-        "schedule": "37    1   *   *   *",
-        "triggerTimes": [
-            5820000,
-            92220000,
-            178620000,
-            265020000,
-            351420000,
-            437820000,
-            524220000,
-            610620000,
-            697020000,
-            783420000
-        ]
-    },
-    {
-        "schedule": "10 10,14 * * *",
-        "triggerTimes": [
-            36600000,
-            51000000,
-            123000000,
-            137400000,
-            209400000,
-            223800000,
-            295800000,
-            310200000,
-            382200000,
-            396600000
-        ]
-    },
-    {
-        "schedule": "2,7,12,17,22,27,32,37,42,47,52,57 * * * *",
-        "triggerTimes": [
-            120000,
-            420000,
-            720000,
-            1020000,
-            1320000,
-            1620000,
-            1920000,
-            2220000,
-            2520000,
-            2820000
-        ]
-    },
-    {
-        "schedule": "0 21 * * *",
-        "triggerTimes": [
-            75600000,
-            162000000,
-            248400000,
-            334800000,
-            421200000,
-            507600000,
-            594000000,
-            680400000,
-            766800000,
-            853200000
-        ]
-    },
-    {
-        "schedule": "25 * * * *",
-        "triggerTimes": [
-            1500000,
-            5100000,
-            8700000,
-            12300000,
-            15900000,
-            19500000,
-            23100000,
-            26700000,
-            30300000,
-            33900000
-        ]
-    },
-    {
-        "schedule": "0 15 * * *,,",
-        "triggerTimes": [
-            54000000,
-            140400000,
-            226800000,
-            313200000,
-            399600000,
-            486000000,
-            572400000,
-            658800000,
-            745200000,
-            831600000
-        ]
-    },
-    {
-        "schedule": "13 9,21 * * *",
-        "triggerTimes": [
-            33180000,
-            76380000,
-            119580000,
-            162780000,
-            205980000,
-            249180000,
-            292380000,
-            335580000,
-            378780000,
-            421980000
-        ]
-    },
-    {
-        "schedule": "10    *   *   *   *",
-        "triggerTimes": [
-            600000,
-            4200000,
-            7800000,
-            11400000,
-            15000000,
-            18600000,
-            22200000,
-            25800000,
-            29400000,
-            33000000
-        ]
-    },
-    {
-        "schedule": "12 18 * * 1,3,5",
-        "triggerTimes": [
-            151920000,
-            411120000,
-            583920000,
-            756720000,
-            1015920000,
-            1188720000,
-            1361520000,
-            1620720000,
-            1793520000,
-            1966320000
-        ]
-    },
-    {
-        "schedule": "0 17-19 * * 1",
-        "triggerTimes": [
-            406800000,
-            410400000,
-            414000000,
-            1011600000,
-            1015200000,
-            1018800000,
-            1616400000,
-            1620000000,
-            1623600000,
-            2221200000
-        ]
-    },
-    {
-        "schedule": "0 10 * * *",
-        "triggerTimes": [
-            36000000,
-            122400000,
-            208800000,
-            295200000,
-            381600000,
-            468000000,
-            554400000,
-            640800000,
-            727200000,
-            813600000
-        ]
-    },
-    {
-        "schedule": "00 00 * * *",
-        "triggerTimes": [
-            86400000,
-            172800000,
-            259200000,
-            345600000,
-            432000000,
-            518400000,
-            604800000,
-            691200000,
-            777600000,
-            864000000
-        ]
-    },
-    {
-        "schedule": "25 16,17,18,22 * * *",
-        "triggerTimes": [
-            59100000,
-            62700000,
-            66300000,
-            80700000,
-            145500000,
-            149100000,
-            152700000,
-            167100000,
-            231900000,
-            235500000
-        ]
-    },
-    {
-        "schedule": "23 6,18 * * *",
-        "triggerTimes": [
-            22980000,
-            66180000,
-            109380000,
-            152580000,
-            195780000,
-            238980000,
-            282180000,
-            325380000,
-            368580000,
-            411780000
-        ]
-    },
-    {
-        "schedule": "17 1,9,17 * * 0",
-        "triggerTimes": [
-            263820000,
-            292620000,
-            321420000,
-            868620000,
-            897420000,
-            926220000,
-            1473420000,
-            1502220000,
-            1531020000,
-            2078220000
-        ]
-    },
-    {
-        "schedule": "00 16 * * *",
-        "triggerTimes": [
-            57600000,
-            144000000,
-            230400000,
-            316800000,
-            403200000,
-            489600000,
-            576000000,
-            662400000,
-            748800000,
-            835200000
-        ]
-    },
-    {
-        "schedule": "*/3 * * * *",
-        "triggerTimes": [
-            180000,
-            360000,
-            540000,
-            720000,
-            900000,
-            1080000,
-            1260000,
-            1440000,
-            1620000,
-            1800000
-        ]
-    },
-    {
-        "schedule": "19    *   *   *   *",
-        "triggerTimes": [
-            1140000,
-            4740000,
-            8340000,
-            11940000,
-            15540000,
-            19140000,
-            22740000,
-            26340000,
-            29940000,
-            33540000
-        ]
-    },
-    {
-        "schedule": "15 * * * *",
-        "triggerTimes": [
-            900000,
-            4500000,
-            8100000,
-            11700000,
-            15300000,
-            18900000,
-            22500000,
-            26100000,
-            29700000,
-            33300000
-        ]
-    },
-    {
-        "schedule": "*/15  *   *   *   *",
-        "triggerTimes": [
-            900000,
-            1800000,
-            2700000,
-            3600000,
-            4500000,
-            5400000,
-            6300000,
-            7200000,
-            8100000,
-            9000000
-        ]
-    },
-    {
-        "schedule": "0 22 * * 1",
-        "triggerTimes": [
-            424800000,
-            1029600000,
-            1634400000,
-            2239200000,
-            2844000000,
-            3448800000,
-            4053600000,
-            4658400000,
-            5263200000,
-            5868000000
-        ]
-    },
-    {
-        "schedule": "15    *   *   *   *",
-        "triggerTimes": [
-            900000,
-            4500000,
-            8100000,
-            11700000,
-            15300000,
-            18900000,
-            22500000,
-            26100000,
-            29700000,
-            33300000
-        ]
-    },
-    {
-        "schedule": "20 04 * * *",
-        "triggerTimes": [
-            15600000,
-            102000000,
-            188400000,
-            274800000,
-            361200000,
-            447600000,
-            534000000,
-            620400000,
-            706800000,
-            793200000
-        ]
-    },
-    {
-        "schedule": "30 0,12 * * *",
-        "triggerTimes": [
-            1800000,
-            45000000,
-            88200000,
-            131400000,
-            174600000,
-            217800000,
-            261000000,
-            304200000,
-            347400000,
-            390600000
-        ]
-    },
-    {
-        "schedule": "15 */4 * * *",
-        "triggerTimes": [
-            900000,
-            15300000,
-            29700000,
-            44100000,
-            58500000,
-            72900000,
-            87300000,
-            101700000,
-            116100000,
-            130500000
-        ]
-    },
-    {
-        "schedule": "29 16,17,18,22 * * *",
-        "triggerTimes": [
-            59340000,
-            62940000,
-            66540000,
-            80940000,
-            145740000,
-            149340000,
-            152940000,
-            167340000,
-            232140000,
-            235740000
-        ]
-    },
-    {
-        "schedule": "37 */3 * * *",
-        "triggerTimes": [
-            2220000,
-            13020000,
-            23820000,
-            34620000,
-            45420000,
-            56220000,
-            67020000,
-            77820000,
-            88620000,
-            99420000
-        ]
-    },
-    {
-        "schedule": "*/15 * * * *",
-        "triggerTimes": [
-            900000,
-            1800000,
-            2700000,
-            3600000,
-            4500000,
-            5400000,
-            6300000,
-            7200000,
-            8100000,
-            9000000
-        ]
-    },
-    {
-        "schedule": "35 23 * * *",
-        "triggerTimes": [
-            84900000,
-            171300000,
-            257700000,
-            344100000,
-            430500000,
-            516900000,
-            603300000,
-            689700000,
-            776100000,
-            862500000
-        ]
-    },
-    {
-        "schedule": "0 17 * * *",
-        "triggerTimes": [
-            61200000,
-            147600000,
-            234000000,
-            320400000,
-            406800000,
-            493200000,
-            579600000,
-            666000000,
-            752400000,
-            838800000
-        ]
-    },
-    {
-        "schedule": "0 22 * * *",
-        "triggerTimes": [
-            79200000,
-            165600000,
-            252000000,
-            338400000,
-            424800000,
-            511200000,
-            597600000,
-            684000000,
-            770400000,
-            856800000
-        ]
-    },
-    {
-        "schedule": "0 11 * * *",
-        "triggerTimes": [
-            39600000,
-            126000000,
-            212400000,
-            298800000,
-            385200000,
-            471600000,
-            558000000,
-            644400000,
-            730800000,
-            817200000
-        ]
-    },
-    {
-        "schedule": "30    *   *   *   *",
-        "triggerTimes": [
-            1800000,
-            5400000,
-            9000000,
-            12600000,
-            16200000,
-            19800000,
-            23400000,
-            27000000,
-            30600000,
-            34200000
-        ]
-    },
-    {
-        "schedule": "41 * * * *",
-        "triggerTimes": [
-            2460000,
-            6060000,
-            9660000,
-            13260000,
-            16860000,
-            20460000,
-            24060000,
-            27660000,
-            31260000,
-            34860000
-        ]
-    },
-    {
-        "schedule": "45 23 * * *",
-        "triggerTimes": [
-            85500000,
-            171900000,
-            258300000,
-            344700000,
-            431100000,
-            517500000,
-            603900000,
-            690300000,
-            776700000,
-            863100000
-        ]
-    },
-    {
-        "schedule": "*/2 * * * *",
-        "triggerTimes": [
-            120000,
-            240000,
-            360000,
-            480000,
-            600000,
-            720000,
-            840000,
-            960000,
-            1080000,
-            1200000
-        ]
-    },
-    {
-        "schedule": "0 0,3,6,9,12,15,18,21 * * *",
-        "triggerTimes": [
-            10800000,
-            21600000,
-            32400000,
-            43200000,
-            54000000,
-            64800000,
-            75600000,
-            86400000,
-            97200000,
-            108000000
-        ]
-    },
-    {
-        "schedule": "0,30 * * * *",
-        "triggerTimes": [
-            1800000,
-            3600000,
-            5400000,
-            7200000,
-            9000000,
-            10800000,
-            12600000,
-            14400000,
-            16200000,
-            18000000
-        ]
-    },
-    {
-        "schedule": "17    *   *   *   *",
-        "triggerTimes": [
-            1020000,
-            4620000,
-            8220000,
-            11820000,
-            15420000,
-            19020000,
-            22620000,
-            26220000,
-            29820000,
-            33420000
-        ]
-    },
-    {
-        "schedule": "30,45 18 * * 1",
-        "triggerTimes": [
-            412200000,
-            413100000,
-            1017000000,
-            1017900000,
-            1621800000,
-            1622700000,
-            2226600000,
-            2227500000,
-            2831400000,
-            2832300000
-        ]
-    },
-    {
-        "schedule": "13,43 * * * *",
-        "triggerTimes": [
-            780000,
-            2580000,
-            4380000,
-            6180000,
-            7980000,
-            9780000,
-            11580000,
-            13380000,
-            15180000,
-            16980000
-        ]
-    },
-    {
-        "schedule": "0 0 10 * *",
-        "triggerTimes": [
-            777600000,
-            3456000000,
-            5875200000,
-            8553600000,
-            11145600000,
-            13824000000,
-            16416000000,
-            19094400000,
-            21772800000,
-            24364800000
-        ]
-    },
-    {
-        "schedule": "13,28,43,58 * * * *",
-        "triggerTimes": [
-            780000,
-            1680000,
-            2580000,
-            3480000,
-            4380000,
-            5280000,
-            6180000,
-            7080000,
-            7980000,
-            8880000
-        ]
-    },
-    {
-        "schedule": "17 9,13,22 * * *",
-        "triggerTimes": [
-            33420000,
-            47820000,
-            80220000,
-            119820000,
-            134220000,
-            166620000,
-            206220000,
-            220620000,
-            253020000,
-            292620000
-        ]
-    },
-    {
-        "schedule": "10 8,12 * * *",
-        "triggerTimes": [
-            29400000,
-            43800000,
-            115800000,
-            130200000,
-            202200000,
-            216600000,
-            288600000,
-            303000000,
-            375000000,
-            389400000
-        ]
-    },
-    {
-        "schedule": "*/5   *   *   *   *",
-        "triggerTimes": [
-            300000,
-            600000,
-            900000,
-            1200000,
-            1500000,
-            1800000,
-            2100000,
-            2400000,
-            2700000,
-            3000000
-        ]
-    },
-    {
-        "schedule": "5,20,35,50 * * * *",
-        "triggerTimes": [
-            300000,
-            1200000,
-            2100000,
-            3000000,
-            3900000,
-            4800000,
-            5700000,
-            6600000,
-            7500000,
-            8400000
-        ]
-    },
-    {
-        "schedule": "00 */2 * * *",
-        "triggerTimes": [
-            7200000,
-            14400000,
-            21600000,
-            28800000,
-            36000000,
-            43200000,
-            50400000,
-            57600000,
-            64800000,
-            72000000
-        ]
-    },
-    {
-        "schedule": "23    *   *   *   *",
-        "triggerTimes": [
-            1380000,
-            4980000,
-            8580000,
-            12180000,
-            15780000,
-            19380000,
-            22980000,
-            26580000,
-            30180000,
-            33780000
-        ]
-    },
-    {
-        "schedule": "7 12 * * *",
-        "triggerTimes": [
-            43620000,
-            130020000,
-            216420000,
-            302820000,
-            389220000,
-            475620000,
-            562020000,
-            648420000,
-            734820000,
-            821220000
-        ]
-    },
-    {
-        "schedule": "*/1 * * * *",
-        "triggerTimes": [
-            60000,
-            120000,
-            180000,
-            240000,
-            300000,
-            360000,
-            420000,
-            480000,
-            540000,
-            600000
-        ]
-    },
-    {
-        "schedule": "0,10,20,30,40,50 * * * *",
-        "triggerTimes": [
-            600000,
-            1200000,
-            1800000,
-            2400000,
-            3000000,
-            3600000,
-            4200000,
-            4800000,
-            5400000,
-            6000000
-        ]
-    },
-    {
-        "schedule": "45 02,06,10,14,18,22 * * *",
-        "triggerTimes": [
-            9900000,
-            24300000,
-            38700000,
-            53100000,
-            67500000,
-            81900000,
-            96300000,
-            110700000,
-            125100000,
-            139500000
-        ]
-    },
-    {
-        "schedule": "39    1   *   *   *",
-        "triggerTimes": [
-            5940000,
-            92340000,
-            178740000,
-            265140000,
-            351540000,
-            437940000,
-            524340000,
-            610740000,
-            697140000,
-            783540000
-        ]
-    },
-    {
-        "schedule": "0 0-2 * * 2-6",
-        "triggerTimes": [
-            3600000,
-            7200000,
-            86400000,
-            90000000,
-            93600000,
-            172800000,
-            176400000,
-            180000000,
-            432000000,
-            435600000
-        ]
-    },
-    {
-        "schedule": "35,50 * * * *",
-        "triggerTimes": [
-            2100000,
-            3000000,
-            5700000,
-            6600000,
-            9300000,
-            10200000,
-            12900000,
-            13800000,
-            16500000,
-            17400000
-        ]
-    },
-    {
-        "schedule": "0 3 1 * *",
-        "triggerTimes": [
-            10800000,
-            2689200000,
-            5108400000,
-            7786800000,
-            10378800000,
-            13057200000,
-            15649200000,
-            18327600000,
-            21006000000,
-            23598000000
-        ]
-    },
-    {
-        "schedule": "5 5 * * *",
-        "triggerTimes": [
-            18300000,
-            104700000,
-            191100000,
-            277500000,
-            363900000,
-            450300000,
-            536700000,
-            623100000,
-            709500000,
-            795900000
-        ]
-    },
-    {
-        "schedule": "18    8   *   *   *",
-        "triggerTimes": [
-            29880000,
-            116280000,
-            202680000,
-            289080000,
-            375480000,
-            461880000,
-            548280000,
-            634680000,
-            721080000,
-            807480000
-        ]
-    },
-    {
-        "schedule": "0 9 * * *",
-        "triggerTimes": [
-            32400000,
-            118800000,
-            205200000,
-            291600000,
-            378000000,
-            464400000,
-            550800000,
-            637200000,
-            723600000,
-            810000000
-        ]
-    },
-    {
-        "schedule": "*/1   *   *   *   *",
-        "triggerTimes": [
-            60000,
-            120000,
-            180000,
-            240000,
-            300000,
-            360000,
-            420000,
-            480000,
-            540000,
-            600000
-        ]
-    },
-    {
-        "schedule": "50 8,12,21 * * *",
-        "triggerTimes": [
-            31800000,
-            46200000,
-            78600000,
-            118200000,
-            132600000,
-            165000000,
-            204600000,
-            219000000,
-            251400000,
-            291000000
-        ]
-    },
-    {
-        "schedule": "29 9,21 * * *",
-        "triggerTimes": [
-            34140000,
-            77340000,
-            120540000,
-            163740000,
-            206940000,
-            250140000,
-            293340000,
-            336540000,
-            379740000,
-            422940000
-        ]
-    },
-    {
-        "schedule": "40 * * * *",
-        "triggerTimes": [
-            2400000,
-            6000000,
-            9600000,
-            13200000,
-            16800000,
-            20400000,
-            24000000,
-            27600000,
-            31200000,
-            34800000
-        ]
-    },
-    {
-        "schedule": "8 21 * * *",
-        "triggerTimes": [
-            76080000,
-            162480000,
-            248880000,
-            335280000,
-            421680000,
-            508080000,
-            594480000,
-            680880000,
-            767280000,
-            853680000
-        ]
-    },
-    {
-        "schedule": "0 6 * * *",
-        "triggerTimes": [
-            21600000,
-            108000000,
-            194400000,
-            280800000,
-            367200000,
-            453600000,
-            540000000,
-            626400000,
-            712800000,
-            799200000
-        ]
-    },
-    {
-        "schedule": "30 0-23/2 * * *",
-        "triggerTimes": [
-            1800000,
-            9000000,
-            16200000,
-            23400000,
-            30600000,
-            37800000,
-            45000000,
-            52200000,
-            59400000,
-            66600000
-        ]
-    },
-    {
-        "schedule": "0 14,22 * * *",
-        "triggerTimes": [
-            50400000,
-            79200000,
-            136800000,
-            165600000,
-            223200000,
-            252000000,
-            309600000,
-            338400000,
-            396000000,
-            424800000
-        ]
-    },
-    {
-        "schedule": "0 */1 * * *",
-        "triggerTimes": [
-            3600000,
-            7200000,
-            10800000,
-            14400000,
-            18000000,
-            21600000,
-            25200000,
-            28800000,
-            32400000,
-            36000000
-        ]
-    },
-    {
-        "schedule": "0 1 * * 1",
-        "triggerTimes": [
-            349200000,
-            954000000,
-            1558800000,
-            2163600000,
-            2768400000,
-            3373200000,
-            3978000000,
-            4582800000,
-            5187600000,
-            5792400000
-        ]
-    },
-    {
-        "schedule": "0 8 * * *",
-        "triggerTimes": [
-            28800000,
-            115200000,
-            201600000,
-            288000000,
-            374400000,
-            460800000,
-            547200000,
-            633600000,
-            720000000,
-            806400000
-        ]
-    },
-    {
-        "schedule": "01 17 * * *",
-        "triggerTimes": [
-            61260000,
-            147660000,
-            234060000,
-            320460000,
-            406860000,
-            493260000,
-            579660000,
-            666060000,
-            752460000,
-            838860000
-        ]
-    },
-    {
-        "schedule": "13    *   *   *   *",
-        "triggerTimes": [
-            780000,
-            4380000,
-            7980000,
-            11580000,
-            15180000,
-            18780000,
-            22380000,
-            25980000,
-            29580000,
-            33180000
-        ]
-    }
-]

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java b/src/test/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
new file mode 100644
index 0000000..0ff8d12
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/CrontabEntryTest.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron;
+
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class CrontabEntryTest {
+  @Test
+  public void testHashCodeAndEquals() {
+    List<CrontabEntry> entries = ImmutableList.of(
+        CrontabEntry.parse("* * * * *"),
+        CrontabEntry.parse("0-59 * * * *"),
+        CrontabEntry.parse("0-57,58,59 * * * *"),
+        CrontabEntry.parse("* 23,1,2,4,0-22 * * *"),
+        CrontabEntry.parse("1-50,0,51-59 * * * sun-sat"));
+
+    for (CrontabEntry lhs : entries) {
+      for (CrontabEntry rhs : entries) {
+        assertEquals(lhs, rhs);
+      }
+    }
+
+    Set<CrontabEntry> equivalentEntries = Sets.newHashSet(entries);
+    assertTrue(equivalentEntries.size() == 1);
+  }
+
+  @Test
+  public void testEqualsCoverage() {
+    assertNotEquals(CrontabEntry.parse("* * * * *"), new Object());
+
+    assertNotEquals(CrontabEntry.parse("* * * * *"), CrontabEntry.parse("1 * * * *"));
+    assertEquals(CrontabEntry.parse("1,2,3 * * * *"), CrontabEntry.parse("1-3 * * * *"));
+
+    assertNotEquals(CrontabEntry.parse("* 0-22 * * *"), CrontabEntry.parse("* * * * *"));
+    assertEquals(CrontabEntry.parse("* 0-23 * * *"), CrontabEntry.parse("* * * * *"));
+
+    assertNotEquals(CrontabEntry.parse("1 1 1-30 * *"), CrontabEntry.parse("1 1 * * *"));
+    assertEquals(CrontabEntry.parse("1 1 1-31 * *"), CrontabEntry.parse("1 1 * * *"));
+
+    assertNotEquals(CrontabEntry.parse("1 1 * JAN,FEB-NOV *"), CrontabEntry.parse("1 1 * * *"));
+    assertEquals(CrontabEntry.parse("1 1 * JAN,FEB-DEC *"), CrontabEntry.parse("1 1 * * *"));
+
+    assertNotEquals(CrontabEntry.parse("* * * * SUN"), CrontabEntry.parse("* * * * SAT"));
+    assertEquals(CrontabEntry.parse("* * * * 0"), CrontabEntry.parse("* * * * SUN"));
+  }
+
+  @Test
+  public void testSkip() {
+    assertEquals(CrontabEntry.parse("*/15 * * * *"), CrontabEntry.parse("0,15,30,45 * * * *"));
+    assertEquals(
+        CrontabEntry.parse("* */2 * * *"),
+        CrontabEntry.parse("0-59 0,2,4,6,8,10,12-23/2  * * *"));
+  }
+
+  @Test
+  public void testToString() {
+    assertEquals("0-58 * * * *", CrontabEntry.parse("0,1-57,58 * * * *").toString());
+    assertEquals("* * * * *", CrontabEntry.parse("* * * * *").toString());
+  }
+
+  @Test
+  public void testWildcards() {
+    CrontabEntry wildcardMinuteEntry = CrontabEntry.parse("* 1 1 1 *");
+    assertEquals("*", wildcardMinuteEntry.getMinuteAsString());
+    assertTrue(wildcardMinuteEntry.hasWildcardMinute());
+    assertFalse(wildcardMinuteEntry.hasWildcardHour());
+    assertFalse(wildcardMinuteEntry.hasWildcardDayOfMonth());
+    assertFalse(wildcardMinuteEntry.hasWildcardMonth());
+    assertTrue(wildcardMinuteEntry.hasWildcardDayOfWeek());
+
+    CrontabEntry wildcardHourEntry = CrontabEntry.parse("1 * 1 1 *");
+    assertEquals("*", wildcardHourEntry.getHourAsString());
+    assertFalse(wildcardHourEntry.hasWildcardMinute());
+    assertTrue(wildcardHourEntry.hasWildcardHour());
+    assertFalse(wildcardHourEntry.hasWildcardDayOfMonth());
+    assertFalse(wildcardHourEntry.hasWildcardMonth());
+    assertTrue(wildcardHourEntry.hasWildcardDayOfWeek());
+
+    CrontabEntry wildcardDayOfMonth = CrontabEntry.parse("1 1 * 1 *");
+    assertEquals("*", wildcardDayOfMonth.getDayOfMonthAsString());
+    assertFalse(wildcardDayOfMonth.hasWildcardMinute());
+    assertFalse(wildcardDayOfMonth.hasWildcardHour());
+    assertTrue(wildcardDayOfMonth.hasWildcardDayOfMonth());
+    assertFalse(wildcardDayOfMonth.hasWildcardMonth());
+    assertTrue(wildcardDayOfMonth.hasWildcardDayOfWeek());
+
+    CrontabEntry wildcardMonth = CrontabEntry.parse("1 1 1 * *");
+    assertEquals("*", wildcardMonth.getMonthAsString());
+    assertFalse(wildcardMonth.hasWildcardMinute());
+    assertFalse(wildcardMonth.hasWildcardHour());
+    assertFalse(wildcardMonth.hasWildcardDayOfMonth());
+    assertTrue(wildcardMonth.hasWildcardMonth());
+    assertTrue(wildcardMonth.hasWildcardDayOfWeek());
+
+    CrontabEntry wildcardDayOfWeek = CrontabEntry.parse("1 1 1 1 *");
+    assertEquals("*", wildcardDayOfWeek.getDayOfWeekAsString());
+    assertFalse(wildcardDayOfWeek.hasWildcardMinute());
+    assertFalse(wildcardDayOfWeek.hasWildcardHour());
+    assertFalse(wildcardDayOfWeek.hasWildcardDayOfMonth());
+    assertFalse(wildcardDayOfWeek.hasWildcardMonth());
+    assertTrue(wildcardDayOfWeek.hasWildcardDayOfWeek());
+  }
+
+  @Test
+  public void testEqualsIsCanonical() {
+    String rawEntry = "* * */3 * *";
+    CrontabEntry input = CrontabEntry.parse(rawEntry);
+    assertNotEquals(
+        rawEntry + " is not the canonical form of " + input,
+        rawEntry,
+        input.toString());
+    assertEquals(
+        "The form returned by toString is canonical",
+        input.toString(),
+        CrontabEntry.parse(input.toString()).toString());
+  }
+
+  @Test
+  public void testBadEntries() {
+    List<String> badPatterns = ImmutableList.of(
+        "* * * * MON-SUN",
+        "* * **",
+        "0-59 0-59 * * *",
+        "1/1 * * * *",
+        "5 5 * MAR-JAN *",
+        "*/0 * * * *",
+        "0-59/0 * * * *",
+        "0-59/60 * * * *",
+        "* * 1 * 1"
+    );
+
+    for (String pattern : badPatterns) {
+      assertNull(CrontabEntry.tryParse(pattern).orNull());
+    }
+  }
+
+  @Test
+  public void testExpectedTriggerPredictionsParse() {
+    for (ExpectedPrediction prediction : ExpectedPrediction.getAll()) {
+      prediction.parseCrontabEntry();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/ExpectedPrediction.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/ExpectedPrediction.java b/src/test/java/org/apache/aurora/scheduler/cron/ExpectedPrediction.java
new file mode 100644
index 0000000..d4caf4e
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/cron/ExpectedPrediction.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2014 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.cron;
+
+import java.io.InputStreamReader;
+import java.util.List;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * A schedule and the expected iteratively-applied prediction results.
+ */
+public final class ExpectedPrediction {
+  private String schedule;
+  private List<Long> triggerTimes;
+
+  ExpectedPrediction() {
+    // GSON constructor.
+  }
+
+  public static List<ExpectedPrediction> getAll() {
+    return new Gson()
+        .fromJson(
+            new InputStreamReader(
+                ExpectedPrediction.class.getResourceAsStream("expected-predictions.json"),
+                Charsets.UTF_8),
+            new TypeToken<List<ExpectedPrediction>>() { }.getType());
+  }
+
+  public String getSchedule() {
+    return schedule;
+  }
+
+  public List<Long> getTriggerTimes() {
+    return ImmutableList.copyOf(triggerTimes);
+  }
+
+  public CrontabEntry parseCrontabEntry() {
+    return CrontabEntry.parse(getSchedule());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3361dbed/src/test/java/org/apache/aurora/scheduler/cron/noop/NoopCronIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/cron/noop/NoopCronIT.java b/src/test/java/org/apache/aurora/scheduler/cron/noop/NoopCronIT.java
deleted file mode 100644
index 0d2f66f..0000000
--- a/src/test/java/org/apache/aurora/scheduler/cron/noop/NoopCronIT.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Copyright 2013 Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.aurora.scheduler.cron.noop;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-import org.apache.aurora.scheduler.cron.CronScheduler;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-public class NoopCronIT {
-  private static final String SCHEDULE = "* * * * *";
-
-  private CronScheduler cronScheduler;
-
-  @Before
-  public void setUp() {
-    Injector injector = Guice.createInjector(new NoopCronModule());
-    cronScheduler = injector.getInstance(CronScheduler.class);
-  }
-
-  @Test
-  public void testLifecycle() throws Exception {
-    cronScheduler.startAsync().awaitRunning();
-    cronScheduler.stopAsync().awaitTerminated();
-  }
-
-  @Test
-  public void testSchedule() throws Exception {
-    cronScheduler.schedule(SCHEDULE, new Runnable() {
-      @Override
-      public void run() {
-        // No-op.
-      }
-    });
-
-    assertEquals(SCHEDULE, cronScheduler.getSchedule(SCHEDULE).orNull());
-
-    cronScheduler.deschedule(SCHEDULE);
-
-    assertNull(cronScheduler.getSchedule(SCHEDULE).orNull());
-  }
-}