Browse Source

Crontab schedules now properly respects CELERY_TIMEZONE. Fixes celery/django-celery#150.

Ask Solem 12 years ago
parent
commit
a41a8892c2
2 changed files with 34 additions and 18 deletions
  1. 13 13
      celery/schedules.py
  2. 21 5
      celery/utils/timeutils.py

+ 13 - 13
celery/schedules.py

@@ -16,9 +16,10 @@ from dateutil.relativedelta import relativedelta
 
 from . import current_app
 from .utils import is_iterable
-from .utils.timeutils import (timedelta_seconds, weekday, maybe_timedelta,
-                              remaining, humanize_seconds, is_naive, to_utc,
-                              timezone)
+from .utils.timeutils import (
+    timedelta_seconds, weekday, maybe_timedelta, remaining,
+    humanize_seconds, timezone, maybe_make_aware
+)
 from .datastructures import AttributeDict
 
 
@@ -38,12 +39,8 @@ class schedule(object):
         return (self.nowfun or current_app.now)()
 
     def remaining_estimate(self, last_run_at):
-        """Returns when the periodic task should run next as a timedelta."""
-        now = self.now()
-        if not is_naive(last_run_at):
-            now = to_utc(now)
         return remaining(last_run_at, self.run_every,
-                         relative=self.relative, now=now)
+                         self.relative, maybe_make_aware(self.now()))
 
     def is_due(self, last_run_at):
         """Returns tuple of two items `(is_due, next_time_to_run)`,
@@ -390,7 +387,10 @@ class crontab(schedule):
         self.day_of_week = self._expand_cronspec(day_of_week, 7)
         self.day_of_month = self._expand_cronspec(day_of_month, 31, 1)
         self.month_of_year = self._expand_cronspec(month_of_year, 12, 1)
-        self.nowfun = nowfun or current_app.now
+        self.nowfun = nowfun
+
+    def now(self):
+        return (self.nowfun or current_app.now)()
 
     def __repr__(self):
         return ('<crontab: %s %s %s %s %s (m/h/d/dM/MY)>' %
@@ -409,8 +409,7 @@ class crontab(schedule):
 
     def remaining_estimate(self, last_run_at):
         """Returns when the periodic task should run next as a timedelta."""
-        if not is_naive(last_run_at):
-            last_run_at = last_run_at.astimezone(timezone.utc)
+        last_run_at = maybe_make_aware(last_run_at)
         dow_num = last_run_at.isoweekday() % 7  # Sunday is day 0, not day 7
 
         execute_this_date = (last_run_at.month in self.month_of_year and
@@ -459,7 +458,8 @@ class crontab(schedule):
                     delta = self._delta_to_next(last_run_at,
                                                 next_hour, next_minute)
 
-        return remaining(last_run_at, delta, now=self.nowfun())
+        return remaining(timezone.to_local(last_run_at),
+                         delta, timezone.to_local(self.now()))
 
     def is_due(self, last_run_at):
         """Returns tuple of two items `(is_due, next_time_to_run)`,
@@ -472,7 +472,7 @@ class crontab(schedule):
         rem = timedelta_seconds(rem_delta)
         due = rem == 0
         if due:
-            rem_delta = self.remaining_estimate(last_run_at=self.nowfun())
+            rem_delta = self.remaining_estimate(self.now())
             rem = timedelta_seconds(rem_delta)
         return due, rem
 

+ 21 - 5
celery/utils/timeutils.py

@@ -49,8 +49,8 @@ class _Zone(object):
 
     def to_local(self, dt, local=None, orig=None):
         if is_naive(dt):
-            dt = set_tz(dt, orig or self.utc)
-        return dt.astimezone(self.tz_or_local(local))
+            dt = make_aware(dt, orig or self.utc)
+        return localize(dt, self.tz_or_local(local))
 
     def get_timezone(self, zone):
         if isinstance(zone, basestring):
@@ -139,7 +139,6 @@ def remaining(start, ends_in, now=None, relative=False):
 
     """
     now = now or datetime.utcnow()
-
     end_date = start + ends_in
     if relative:
         end_date = delta_resolution(end_date, ends_in)
@@ -201,7 +200,7 @@ def is_naive(dt):
     return dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None
 
 
-def set_tz(dt, tz):
+def make_aware(dt, tz):
     """Sets the timezone for a datetime object."""
     try:
         localize = tz.localize
@@ -212,6 +211,23 @@ def set_tz(dt, tz):
         return localize(dt, is_dst=None)
 
 
+def localize(dt, tz):
+    """Convert aware datetime to another timezone."""
+    dt = dt.astimezone(tz)
+    try:
+        normalize = tz.normalize
+    except AttributeError:
+        return dt
+    else:
+        return normalize(dt)  # pytz
+
+
 def to_utc(dt):
     """Converts naive datetime to UTC"""
-    return set_tz(dt, timezone.utc)
+    return make_aware(dt, timezone.utc)
+
+
+def maybe_make_aware(dt, tz=None):
+    if is_naive(dt):
+        return to_utc(dt)
+    return localize(dt, timezone.utc if tz is None else tz)