Bug 1453067 Support for days (of week or month) in taskcluster cron r=dustin
authorSimon Fraser <sfraser@mozilla.com>
Thu, 12 Apr 2018 12:39:20 +0100
changeset 413001 5ea0755f98afcf0b007b018fa79285089c01c789
parent 413000 87b552f9c09b68a255eeb84c5c554384b0c1231a
child 413002 1f351e5132ab4f21dc117dc737c8e24e19d2e21a
push id33829
push userarchaeopteryx@coole-files.de
push dateThu, 12 Apr 2018 19:20:32 +0000
treeherdermozilla-central@17dda59473c3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdustin
bugs1453067
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1453067 Support for days (of week or month) in taskcluster cron r=dustin 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, root):
 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, root):
     params = params.copy()
     params['job_name'] = job_name
 
--- 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(root):
     # 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}'], cwd=root)
--- 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()