Welcome to little lamb

Code » slicd » commit 7fb28b3

Add "special DST" mode

author Olivier Brunel
2016-04-06 17:22:50 UTC
committer Olivier Brunel
2016-04-30 12:30:10 UTC
parent 93edd2a136ed10094f4b73445e3fd3be4cfc466d

Add "special DST" mode

doc/slicd-parser.pod +3 -0
doc/slicd-sched.pod +17 -0
src/include/slicd/fields.h +3 -1
src/include/slicd/job.h +5 -0
src/libslicd/slicd_add_job_from_cronline.c +6 -0
src/libslicd/slicd_job.c +17 -0
src/libslicd/slicd_job_next_run.c +186 -24

diff --git a/doc/slicd-parser.pod b/doc/slicd-parser.pod
index 9c84f53..53b3064 100644
--- a/doc/slicd-parser.pod
+++ b/doc/slicd-parser.pod
@@ -81,6 +81,9 @@ The time-and-date fields are, in order:
     month               1-12 (or names, see below)
     day of the week     0-6 (0=Sunday, 1=Monday, etc; or names, see below)
 
+The hour field can have its value prefixed with an exclamation point (!) to
+enable the special DST mode for the job; See B<slicd-sched>(1) for more.
+
 Range of values are allowed, separating two values with a hyphen (-). The
 specified range is inclusive, and must always have the lower value first.
 
diff --git a/doc/slicd-sched.pod b/doc/slicd-sched.pod
index 48bd3f7..95cf2f7 100644
--- a/doc/slicd-sched.pod
+++ b/doc/slicd-sched.pod
@@ -72,6 +72,23 @@ to run "missed" jobs either. For example our job set to run every hour at minute
 30, when the clock goes from 01:59 (DST off) to 03:00 (DST on), would simply run
 at 01:30 (DST off) then an hour later at 03:30 (DST on).
 
+=head2 Special DST mode
+
+A special DST handling mode can be activated on a per-job basis (see
+B<slicd-parser>(1)), in which case time changes due to DST are handled
+differently than described above.
+
+When time jumps backwards, jobs will only run during the "first pass" - or when
+DST was activated. When the time "repeats" but with DST deactivated, they will
+not run. So using the same example, our job would run at 02:30 (DST on), and
+then only two hours later, at 03:30 (DST off).
+
+When time jumps forward, jobs that would have run at least once during the
+"missed" interval will run at the first minute of DST. Again, with our usual
+example it would have the job run at 01:30 (DST off), then at 03:00 (DST on) -
+because of a "missed run" at the non-existing time of 02:30 - and back as usual
+at 03:30 (DST off).
+
 =head1 SIGNALS
 
 The following signals can be used :
diff --git a/src/include/slicd/fields.h b/src/include/slicd/fields.h
index 71b3676..06c18e2 100644
--- a/src/include/slicd/fields.h
+++ b/src/include/slicd/fields.h
@@ -30,8 +30,9 @@
  * 12   months (0-11)
  *  7   days of week (0-6; 0=sunday, 1=monday, etc)
  *  1   days combo: set when both days & dow are set, i.e. the Nth dow
+ *  1   special DST: to enable special handling for DST changes
  *
- * 135 bits total; hence an array of 17 char-s (leaving 1 bit unused)
+ * 136 bits total; hence an array of 17 char-s
  *
  * Both DAYS & DOW set means that DAYS is limited to 0-6 which stands for the
  * 1st, 2nd, ... till the 5th & then last DOW of the month.
@@ -42,6 +43,7 @@
 #define _SLICD_BITS_OFFSET_MONTHS       _SLICD_BITS_OFFSET_DAYS + 31
 #define _SLICD_BITS_OFFSET_DOW          _SLICD_BITS_OFFSET_MONTHS + 12
 #define _SLICD_BIT_DAYS_COMBO           _SLICD_BITS_OFFSET_DOW + 7
+#define _SLICD_BIT_DST_SPECIAL          _SLICD_BIT_DAYS_COMBO + 1
 
 static struct
 {
diff --git a/src/include/slicd/job.h b/src/include/slicd/job.h
index c15fed5..1984cf4 100644
--- a/src/include/slicd/job.h
+++ b/src/include/slicd/job.h
@@ -51,6 +51,11 @@ extern int slicd_job_has_days_combo (slicd_job_t    *job);
 extern int slicd_job_set_days_combo (slicd_job_t    *job,
                                      int             what);
 
+extern int slicd_job_has_dst_special(slicd_job_t    *job);
+
+extern int slicd_job_set_dst_special(slicd_job_t    *job,
+                                     int             what);
+
 extern int slicd_job_clearset       (slicd_job_t    *job,
                                      slicd_field_t   field,
                                      int             from,
diff --git a/src/libslicd/slicd_add_job_from_cronline.c b/src/libslicd/slicd_add_job_from_cronline.c
index 4e61cd3..e2c7d66 100644
--- a/src/libslicd/slicd_add_job_from_cronline.c
+++ b/src/libslicd/slicd_add_job_from_cronline.c
@@ -86,6 +86,12 @@ parse_interval (slicd_job_t *job, slicd_field_t field, const char **s)
     int max = min + _slicd_fields[field].max;
     int from, to, step;
 
+    if (field == SLICD_HOURS && **s == '!')
+    {
+        slicd_job_set_dst_special (job, 1);
+        ++*s;
+    }
+
 again:
     if (**s == '*')
     {
diff --git a/src/libslicd/slicd_job.c b/src/libslicd/slicd_job.c
index 6d10685..380bd27 100644
--- a/src/libslicd/slicd_job.c
+++ b/src/libslicd/slicd_job.c
@@ -64,6 +64,23 @@ slicd_job_set_days_combo (slicd_job_t    *job,
     return 0;
 }
 
+int
+slicd_job_has_dst_special (slicd_job_t    *job)
+{
+    assert (job != NULL);
+    return bitarray_isset (job->bits, _SLICD_BIT_DST_SPECIAL);
+}
+
+int
+slicd_job_set_dst_special (slicd_job_t    *job,
+                           int             what)
+{
+    assert (job != NULL);
+
+    bitarray_clearsetn (job->bits, _SLICD_BIT_DST_SPECIAL, 1, what);
+    return 0;
+}
+
 int
 slicd_job_clearset (slicd_job_t    *job,
                     slicd_field_t   field,
diff --git a/src/libslicd/slicd_job_next_run.c b/src/libslicd/slicd_job_next_run.c
index 66421b3..9c1031a 100644
--- a/src/libslicd/slicd_job_next_run.c
+++ b/src/libslicd/slicd_job_next_run.c
@@ -74,12 +74,127 @@ first_minute_of_dst (time_t time, struct tm *time_tm)
     }
 }
 
+static int
+days_combo_match (slicd_job_t *job, struct tm *time)
+{
+    int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+    struct tm first;
+    int i;
+
+    days[1] = is_leap (time->tm_year) ? 29 : 28;
+
+    byte_copy (&first, sizeof (struct tm), time);
+    first.tm_mday = 1;
+    /* get tm_wday */
+    if (mktime (&first) == (time_t) -1)
+        return -1;
+
+    /* let's get first.tm_mday to be the first next->tm_wday of the month */
+    first.tm_mday = time->tm_wday;
+    if (first.tm_mday < first.tm_wday)
+        first.tm_mday += 7;
+    first.tm_mday += 1 - first.tm_wday;
+
+    for (i = 1; i < 6; ++i, first.tm_mday += 7)
+        if (first.tm_mday == time->tm_mday)
+        {
+            if (slicd_job_has (job, SLICD_DAYS, i)
+                    || (first.tm_mday + 7 > days[time->tm_mon]
+                        && slicd_job_has (job, SLICD_DAYS, 6)))
+                return 1;
+            else
+                return 0;
+        }
+
+    /* silence warning -- the "if" in the "for" loop above always matches */
+    return -1;
+}
+
+static int
+would_have_ran (slicd_job_t *job, struct tm *first)
+{
+    int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+    struct tm tm;
+    int n;
+
+    memcpy (&tm, first, sizeof (struct tm));
+
+    --tm.tm_min;
+    if (mktime (&tm) == (time_t) -1)
+        return -1;
+
+    days[1] = is_leap (tm.tm_year) ? 29 : 28;
+    if (++tm.tm_min == 60)
+    {
+        tm.tm_min = 0;
+        if (++tm.tm_hour == 24)
+        {
+            tm.tm_hour = 0;
+            if (++tm.tm_mday > days[tm.tm_mon])
+            {
+                tm.tm_mday = 1;
+                if (++tm.tm_mon > 11)
+                {
+                    tm.tm_mon = 0;
+                    ++tm.tm_year;
+                    days[1] = is_leap (tm.tm_year) ? 29 : 28;
+                }
+            }
+        }
+    }
+
+    n = slicd_job_first (job, SLICD_MONTHS, tm.tm_mon + 1, 12, 1);
+    if (n > tm.tm_mon + 1)
+        return 0;
+
+    if (!slicd_job_has (job, SLICD_DOW, tm.tm_wday))
+        return 0;
+
+    if (slicd_job_has_days_combo (job))
+    {
+        n = days_combo_match (job, first);
+        if (n <= 0)
+            return n;
+    }
+    else
+    {
+        n = slicd_job_first (job, SLICD_DAYS, tm.tm_mday, days[tm.tm_mon], 1);
+        if (n > tm.tm_mday)
+            return 0;
+    }
+
+again:
+    n = slicd_job_first (job, SLICD_HOURS, tm.tm_hour, 23, 1);
+    if (n > first->tm_hour)
+        return 0;
+
+    if (n == first->tm_hour)
+    {
+        n = slicd_job_first (job, SLICD_MINUTES, 0, first->tm_min, 1);
+        if (n >= first->tm_min)
+            return 0;
+    }
+    else
+    {
+        n = slicd_job_first (job, SLICD_MINUTES, tm.tm_min, 59, 1);
+        if (n > 59)
+        {
+            if (++tm.tm_hour > 23)
+                return 0;
+            tm.tm_min = 0;
+            goto again;
+        }
+    }
+
+    return 1;
+}
+
 time_t
 slicd_job_next_run (slicd_job_t *job, struct tm *next)
 {
     int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
     time_t time;
-    int dst = -1;
+    int dst = -2;
     int n;
 
 again:
@@ -87,8 +202,31 @@ again:
     time = mktime (next);
     if (time == (time_t) -1)
         return time;
-    else if (dst == -1)
+    else if (dst < 0)
+    {
+        /* DST special: if DST is on for the reference time, check if the minute
+         * before was off, in which case we're right after the "skipped period"
+         * and need to see if the job would have been ran then, to run it now
+         * instead ("catching up") */
+        if (dst == -2 && slicd_job_has_dst_special (job) && next->tm_isdst == 1)
+        {
+            time_t t = time - 60;
+            struct tm *tm;
+
+            tm = localtime (&t);
+            if (!tm)
+                return (time_t) -1;
+            if (tm->tm_isdst == 0)
+            {
+                n = would_have_ran (job, next);
+                if (n < 0)
+                    return (time_t) -1;
+                else if (n)
+                    return time;
+            }
+        }
         dst = next->tm_isdst;
+    }
     else if (next->tm_isdst != dst)
     {
         /* we changed DST state, set next to the first minute with this new DST
@@ -100,6 +238,17 @@ again:
         if (time == (time_t) -1)
             return time;
         dst = next->tm_isdst;
+
+        /* DST special: if DST is on, check if it would have ran during the
+         * "skipped period" and if so, match this first minute of new DST */
+        if (dst == 1 && slicd_job_has_dst_special (job))
+        {
+            n = would_have_ran (job, next);
+            if (n < 0)
+                return (time_t) -1;
+            else if (n)
+                return time;
+        }
     }
 
     days[1] = is_leap (next->tm_year) ? 29 : 28;
@@ -190,31 +339,44 @@ bump_day:
 
     if (slicd_job_has_days_combo (job))
     {
-        struct tm first;
-        int i;
-
-        byte_copy (&first, sizeof (first), next);
-        first.tm_mday = 1;
-        /* get tm_wday */
-        if (mktime (&first) == (time_t) -1)
+        n = days_combo_match (job, next);
+        if (n < 0)
             return (time_t) -1;
+        else if (!n)
+            goto bump_day;
+    }
 
-        /* let's get first.tm_mday to be the first next->tm_wday of the month */
-        first.tm_mday = next->tm_wday;
-        if (first.tm_mday < first.tm_wday)
-            first.tm_mday += 7;
-        first.tm_mday += 1 - first.tm_wday;
+    /* DST special: if DST is off, get the first minute since it's been off, and
+     * make sure this job isn't in the "repeating time period" and if so, start
+     * again after said period */
+    if (slicd_job_has_dst_special (job) && next->tm_isdst == 0)
+    {
+        time_t t;
+        struct tm tm;
 
-        for (i = 1; i < 6; ++i, first.tm_mday += 7)
-            if (first.tm_mday == next->tm_mday)
-            {
-                if (slicd_job_has (job, SLICD_DAYS, i)
-                        || (first.tm_mday + 7 > days[next->tm_mon]
-                            && slicd_job_has (job, SLICD_DAYS, 6)))
-                    break;
-                else
-                    goto bump_day;
-            }
+        memcpy (&tm, next, sizeof (struct tm));
+        if (first_minute_of_dst (time, &tm) == (time_t) -1)
+            return (time_t) -1;
+        /* move to last minute w/ DST on */
+        --tm.tm_min;
+        if (mktime (&tm) == (time_t) -1)
+            return (time_t) -1;
+        /* and get to the same minute but w/ DST off, i.e. last minute to be
+         * "repeated" */
+        tm.tm_isdst = 0;
+        t = mktime (&tm);
+        if (t == (time_t) -1)
+            return t;
+
+        if (time <= t)
+        {
+            /* update next.. */
+            memcpy (next, &tm, sizeof (struct tm));
+            /* ..to the first minute after the "repeating period" */
+            ++next->tm_min;
+            dst = 0;
+            goto again;
+        }
     }
 
     return time;