Bug 1453067 - Support for days (of week or month) in taskcluster cron. r=dustin, a=release
authorSimon Fraser <sfraser@mozilla.com>
Thu, 12 Apr 2018 12:39:20 +0100
changeset 806004 6809bbed542c8fab1459e65cf5afd1e406b53a17
parent 806003 fe272f41ed069ec44f37b5fd7f1b8ad7c4cd41a8
child 806005 679f356c64583a6c4203844770e574c6e4136bdc
push id112832
push userbballo@mozilla.com
push dateFri, 08 Jun 2018 21:11:22 +0000
reviewersdustin, release
bugs1453067
milestone60.0.2
Bug 1453067 - Support for days (of week or month) in taskcluster cron. r=dustin, a=release Summary: We need to run things less often than once a day, so adding support for days to taskcluster cron. 'day' is the day of the month, 'weekday' is used as a datetime.weekday (not isoweekday), or a string comparable to strftime('%A') or strftime('%a') Reviewers: dustin Reviewed By: dustin Bug #: 1453067 Differential Revision: https://phabricator.services.mozilla.com/D903
taskcluster/taskgraph/cron/__init__.py
taskcluster/taskgraph/cron/schema.py
taskcluster/taskgraph/cron/util.py
taskcluster/taskgraph/test/test_cron_util.py
--- a/taskcluster/taskgraph/cron/__init__.py
+++ b/taskcluster/taskgraph/cron/__init__.py
@@ -50,18 +50,17 @@ def load_jobs(params):
 def should_run(job, params):
     run_on_projects = job.get('run-on-projects', ['all'])
     if not match_run_on_projects(params['project'], run_on_projects):
         return False
     # Resolve when key here, so we don't require it before we know that we
     # actually want to run on this branch.
     resolve_keyed_by(job, 'when', 'Cron job ' + job['name'],
                      project=params['project'])
-    if not any(match_utc(params, hour=sched.get('hour'), minute=sched.get('minute'))
-               for sched in job.get('when', [])):
+    if not any(match_utc(params, sched=sched) for sched in job.get('when', [])):
         return False
     return True
 
 
 def run_job(job_name, job, params):
     params['job_name'] = job_name
 
     try:
--- a/taskcluster/taskgraph/cron/schema.py
+++ b/taskcluster/taskgraph/cron/schema.py
@@ -47,15 +47,25 @@ cron_yml_schema = Schema({
         'run-on-projects': [basestring],
 
         # Array of times at which this task should run.  These *must* be a
         # multiple of 15 minutes, the minimum scheduling interval.  This field
         # can be keyed by project so that each project has a different schedule
         # for the same job.
         'when': optionally_keyed_by(
             'project',
-            [{'hour': int, 'minute': All(int, even_15_minutes)}]),
+            [
+                {
+                    'hour': int,
+                    'minute': All(int, even_15_minutes),
+                    # You probably don't want both day and weekday.
+                    'day': int,  # Day of the month, as used by datetime.
+                    'weekday': Any('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
+                                   'Saturday', 'Sunday')
+                }
+            ]
+        ),
     }],
 })
 
 
 def validate(cron_yml):
     validate_schema(cron_yml_schema, cron_yml, "Invalid .cron.yml:")
--- a/taskcluster/taskgraph/cron/util.py
+++ b/taskcluster/taskgraph/cron/util.py
@@ -5,28 +5,43 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import subprocess
 
 
-def match_utc(params, hour=None, minute=None):
-    """ Return True if params['time'] matches the given hour and minute.
-    If hour is not specified, any hour will match.  If minute is not
-    specified, then every multiple of fifteen minutes will match.  Times
-    not an even multiple of fifteen minutes will result in an exception
-    (since they would never run)."""
-    if minute is not None and minute % 15 != 0:
+def match_utc(params, sched):
+    """Return True if params['time'] matches the given schedule.
+
+    If minute is not specified, then every multiple of fifteen minutes will match.
+    Times not an even multiple of fifteen minutes will result in an exception
+    (since they would never run).
+    If hour is not specified, any hour will match. Similar for day and weekday.
+    """
+    if sched.get('minute') and sched.get('minute') % 15 != 0:
         raise Exception("cron jobs only run on multiples of 15 minutes past the hour")
-    if hour is not None and params['time'].hour != hour:
+
+    if sched.get('minute') is not None and sched.get('minute') != params['time'].minute:
+        return False
+
+    if sched.get('hour') is not None and sched.get('hour') != params['time'].hour:
+        return False
+
+    if sched.get('day') is not None and sched.get('day') != params['time'].day:
         return False
-    if minute is not None and params['time'].minute != minute:
+
+    if isinstance(sched.get('weekday'), str) or isinstance(sched.get('weekday'), unicode):
+        if sched.get('weekday', str()).lower() != params['time'].strftime('%A').lower():
+            return False
+    elif sched.get('weekday') is not None:
+        # don't accept other values.
         return False
+
     return True
 
 
 def calculate_head_rev(options):
     # we assume that run-task has correctly checked out the revision indicated by
     # GECKO_HEAD_REF, so all that remains is to see what the current revision is.
     # Mercurial refers to that as `.`.
     return subprocess.check_output(['hg', 'log', '-r', '.', '-T', '{node}'])
--- a/taskcluster/taskgraph/test/test_cron_util.py
+++ b/taskcluster/taskgraph/test/test_cron_util.py
@@ -13,54 +13,66 @@ from taskgraph.cron.util import (
     match_utc,
 )
 
 
 class TestMatchUtc(unittest.TestCase):
 
     def test_hour_minute(self):
         params = {'time': datetime.datetime(2017, 1, 26, 16, 30, 0)}
-        self.assertFalse(match_utc(params, hour=4, minute=30))
-        self.assertTrue(match_utc(params, hour=16, minute=30))
-        self.assertFalse(match_utc(params, hour=16, minute=0))
+        self.assertFalse(match_utc(params, {'hour': 4, 'minute': 30}))
+        self.assertTrue(match_utc(params, {'hour': 16, 'minute': 30}))
+        self.assertFalse(match_utc(params, {'hour': 16, 'minute': 0}))
 
     def test_hour_only(self):
         params = {'time': datetime.datetime(2017, 1, 26, 16, 0, 0)}
-        self.assertFalse(match_utc(params, hour=0))
-        self.assertFalse(match_utc(params, hour=4))
-        self.assertTrue(match_utc(params, hour=16))
+        self.assertFalse(match_utc(params, {'hour': 0}))
+        self.assertFalse(match_utc(params, {'hour': 4}))
+        self.assertTrue(match_utc(params, {'hour': 16}))
         params = {'time': datetime.datetime(2017, 1, 26, 16, 15, 0)}
-        self.assertFalse(match_utc(params, hour=0))
-        self.assertFalse(match_utc(params, hour=4))
-        self.assertTrue(match_utc(params, hour=16))
+        self.assertFalse(match_utc(params, {'hour': 0}))
+        self.assertFalse(match_utc(params, {'hour': 4}))
+        self.assertTrue(match_utc(params, {'hour': 16}))
         params = {'time': datetime.datetime(2017, 1, 26, 16, 30, 0)}
-        self.assertFalse(match_utc(params, hour=0))
-        self.assertFalse(match_utc(params, hour=4))
-        self.assertTrue(match_utc(params, hour=16))
+        self.assertFalse(match_utc(params, {'hour': 0}))
+        self.assertFalse(match_utc(params, {'hour': 4}))
+        self.assertTrue(match_utc(params, {'hour': 16}))
         params = {'time': datetime.datetime(2017, 1, 26, 16, 45, 0)}
-        self.assertFalse(match_utc(params, hour=0))
-        self.assertFalse(match_utc(params, hour=4))
-        self.assertTrue(match_utc(params, hour=16))
+        self.assertFalse(match_utc(params, {'hour': 0}))
+        self.assertFalse(match_utc(params, {'hour': 4}))
+        self.assertTrue(match_utc(params, {'hour': 16}))
 
     def test_minute_only(self):
         params = {'time': datetime.datetime(2017, 1, 26, 13, 0, 0)}
-        self.assertTrue(match_utc(params, minute=0))
-        self.assertFalse(match_utc(params, minute=15))
-        self.assertFalse(match_utc(params, minute=30))
-        self.assertFalse(match_utc(params, minute=45))
+        self.assertTrue(match_utc(params, {'minute': 0}))
+        self.assertFalse(match_utc(params, {'minute': 15}))
+        self.assertFalse(match_utc(params, {'minute': 30}))
+        self.assertFalse(match_utc(params, {'minute': 45}))
 
     def test_zeroes(self):
         params = {'time': datetime.datetime(2017, 1, 26, 0, 0, 0)}
-        self.assertTrue(match_utc(params, minute=0))
-        self.assertTrue(match_utc(params, hour=0))
-        self.assertFalse(match_utc(params, hour=1))
-        self.assertFalse(match_utc(params, minute=15))
-        self.assertFalse(match_utc(params, minute=30))
-        self.assertFalse(match_utc(params, minute=45))
+        self.assertTrue(match_utc(params, {'minute': 0}))
+        self.assertTrue(match_utc(params, {'hour': 0}))
+        self.assertFalse(match_utc(params, {'hour': 1}))
+        self.assertFalse(match_utc(params, {'minute': 15}))
+        self.assertFalse(match_utc(params, {'minute': 30}))
+        self.assertFalse(match_utc(params, {'minute': 45}))
 
     def test_invalid_minute(self):
         params = {'time': datetime.datetime(2017, 1, 26, 13, 0, 0)}
         self.assertRaises(Exception, lambda:
-                          match_utc(params, minute=1))
+                          match_utc(params, {'minute': 1}))
+
+    def test_day_hour_minute(self):
+        params = {'time': datetime.datetime(2017, 1, 26, 16, 30, 0)}
+        self.assertFalse(match_utc(params, {'day': 25, 'hour': 16, 'minute': 30}))
+        self.assertTrue(match_utc(params, {'day': 26, 'hour': 16, 'minute': 30}))
+        self.assertFalse(match_utc(params, {'day': 26, 'hour': 16, 'minute': 0}))
+
+    def test_weekday_hour_minute(self):
+        params = {'time': datetime.datetime(2017, 1, 26, 16, 30, 0)}
+        self.assertFalse(match_utc(params, {'weekday': 'Wednesday', 'hour': 16, 'minute': 30}))
+        self.assertTrue(match_utc(params, {'weekday': 'Thursday', 'hour': 16, 'minute': 30}))
+        self.assertFalse(match_utc(params, {'weekday': 'Thursday', 'hour': 16, 'minute': 0}))
 
 
 if __name__ == '__main__':
     main()