Browse Source

Fixes periodic task when UTC is disabled. Closes #952

Ask Solem 12 years ago
parent
commit
f72fed4583
3 changed files with 102 additions and 15 deletions
  1. 19 4
      celery/beat.py
  2. 18 10
      celery/schedules.py
  3. 65 1
      celery/utils/timeutils.py

+ 19 - 4
celery/beat.py

@@ -32,7 +32,8 @@ from .utils.timeutils import humanize_seconds
 from .utils.log import get_logger
 
 logger = get_logger(__name__)
-debug, info, error = logger.debug, logger.info, logger.error
+debug, info, error, warning = (logger.debug, logger.info,
+                               logger.error, logger.warning)
 
 DEFAULT_MAX_INTERVAL = 300  # 5 minutes
 
@@ -341,17 +342,31 @@ class PersistentScheduler(Scheduler):
                                                 writeback=True)
         else:
             if '__version__' not in self._store:
+                warning('Reset: Account for new __version__ field')
                 self._store.clear()   # remove schedule at 2.2.2 upgrade.
             if 'tz' not in self._store:
+                warning('Reset: Account for new tz field')
                 self._store.clear()   # remove schedule at 3.0.8 upgrade
+            if 'utc_enabled' not in self._store:
+                warning('Reset: Account for new utc_enabled field')
+                self._store.clear()   # remove schedule at 3.0.9 upgrade
+
         tz = self.app.conf.CELERY_TIMEZONE
-        current_tz = self._store.get('tz')
-        if current_tz is not None and current_tz != tz:
+        stored_tz = self._store.get('tz')
+        if stored_tz is not None and stored_tz != tz:
+            warning('Reset: Timezone changed from %r to %r', stored_tz, tz)
             self._store.clear()   # Timezone changed, reset db!
+        utc = self.app.conf.CELERY_ENABLE_UTC
+        stored_utc = self._store.get('utc_enabled')
+        if stored_utc is not None and stored_utc != utc:
+            choices = {True: 'enabled', False: 'disabled'}
+            warning('Reset: UTC changed from %s to %s',
+                    choices[stored_utc], choices[utc])
+            self._store.clear()   # UTC setting changed, reset db!
         entries = self._store.setdefault('entries', {})
         self.merge_inplace(self.app.conf.CELERYBEAT_SCHEDULE)
         self.install_default_entries(self.schedule)
-        self._store.update(__version__=__version__, utc=True, tz=tz)
+        self._store.update(__version__=__version__, tz=tz, utc_enabled=utc)
         self.sync()
         debug('Current schedule:\n' + '\n'.join(repr(entry)
                                     for entry in entries.itervalues()))

+ 18 - 10
celery/schedules.py

@@ -12,7 +12,9 @@ from __future__ import absolute_import
 import re
 
 from datetime import datetime, timedelta
+
 from dateutil.relativedelta import relativedelta
+from kombu.utils import cached_property
 
 from . import current_app
 from .utils import is_iterable
@@ -28,7 +30,6 @@ class ParseException(Exception):
 
 
 class schedule(object):
-    _app = None
     relative = False
 
     def __init__(self, run_every=None, relative=False, nowfun=None):
@@ -79,7 +80,7 @@ class schedule(object):
         return False, rem
 
     def maybe_make_aware(self, dt):
-        if self.app.conf.CELERY_ENABLE_UTC:
+        if self.utc_enabled:
             return maybe_make_aware(dt, self.tz)
         return dt
 
@@ -99,15 +100,22 @@ class schedule(object):
     def human_seconds(self):
         return humanize_seconds(self.seconds)
 
-    @property
+    @cached_property
     def app(self):
-        if self._app is None:
-            self._app = current_app._get_current_object()
-        return self._app
+        return current_app._get_current_object()
 
-    @property
+    @cached_property
     def tz(self):
-        return self.app.conf.CELERY_TIMEZONE
+        return timezone.get_timezone(self.app.conf.CELERY_TIMEZONE)
+
+    @cached_property
+    def utc_enabled(self):
+        return self.app.conf.CELERY_ENABLE_UTC
+
+    @cached_property
+    def to_local(self):
+        return (timezone.to_local if self.utc_enabled
+                                  else timezone.to_local_fallback)
 
 
 class crontab_parser(object):
@@ -476,8 +484,8 @@ class crontab(schedule):
                     delta = self._delta_to_next(last_run_at,
                                                 next_hour, next_minute)
 
-        return remaining(timezone.to_local(last_run_at, tz),
-                         delta, timezone.to_local(self.now(), tz))
+        return remaining(self.to_local(last_run_at, tz),
+                         delta, self.to_local(self.now(), tz))
 
     def is_due(self, last_run_at):
         """Returns tuple of two items `(is_due, next_time_to_run)`,

+ 65 - 1
celery/utils/timeutils.py

@@ -8,9 +8,11 @@
 """
 from __future__ import absolute_import
 
+import time as _time
+
 from kombu.utils import cached_property
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, tzinfo
 from dateutil import tz
 from dateutil.parser import parse as parse_iso8601
 
@@ -39,6 +41,63 @@ TIME_UNITS = (('day', 60 * 60 * 24.0, lambda n: '%.2f' % n),
               ('minute', 60.0, lambda n: '%.2f' % n),
               ('second', 1.0, lambda n: '%.2f' % n))
 
+ZERO = timedelta(0)
+
+_local_timezone = None
+
+
+class LocalTimezone(tzinfo):
+    """
+    Local time implementation taken from Python's docs.
+
+    Used only when pytz isn't available, and most likely inaccurate. If you're
+    having trouble with this class, don't waste your time, just install pytz.
+    """
+
+    def __init__(self):
+        # This code is moved in __init__ to execute it as late as possible
+        # See get_default_timezone().
+        self.STDOFFSET = timedelta(seconds=-_time.timezone)
+        if _time.daylight:
+            self.DSTOFFSET = timedelta(seconds=-_time.altzone)
+        else:
+            self.DSTOFFSET = self.STDOFFSET
+        self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET
+        tzinfo.__init__(self)
+
+    def __repr__(self):
+        return "<LocalTimezone>"
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return self.DSTOFFSET
+        else:
+            return self.STDOFFSET
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            return self.DSTDIFF
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        return _time.tzname[self._isdst(dt)]
+
+    def _isdst(self, dt):
+        tt = (dt.year, dt.month, dt.day,
+              dt.hour, dt.minute, dt.second,
+              dt.weekday(), 0, 0)
+        stamp = _time.mktime(tt)
+        tt = _time.localtime(stamp)
+        return tt.tm_isdst > 0
+
+
+def _get_local_timezone():
+    global _local_timezone
+    if _local_timezone is None:
+        _local_timezone = LocalTimezone()
+    return _local_timezone
+
 
 class _Zone(object):
 
@@ -52,6 +111,11 @@ class _Zone(object):
             dt = make_aware(dt, orig or self.utc)
         return localize(dt, self.tz_or_local(local))
 
+    def to_local_fallback(self, dt, *args, **kwargs):
+        if is_naive(dt):
+            return make_aware(dt, _get_local_timezone())
+        return localize(dt, _get_local_timezone())
+
     def get_timezone(self, zone):
         if isinstance(zone, basestring):
             if pytz is None: