Skip to content

Commit 747ac59

Browse files
vytisbauvipy
authored andcommitted
Timezone handling fixes (#456)
* Simplified timezone handling code. * Use integer division. * Fix eta and expires timezone handling in Camera. maybe_iso8601() returns an aware datetime if it can. Correctly make it naive if `USE_TZ = False`. * Fix timestamp timezone handling in Camera. Removed reliance on CELERY_ENABLE_UTC, because when it is not defined it is assumed False, unlike in celery. Also some code cleanup. * Show datetimes in local timezone in admin. * Add Changelog entry.
1 parent ec2c3e5 commit 747ac59

File tree

8 files changed

+102
-83
lines changed

8 files changed

+102
-83
lines changed

Changelog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737

3838
Fix contributed by Rockallite Wulf.
3939

40+
- Fix timezone handling in Camera and admin.
41+
42+
Fix contributed by Vytis Banaitis.
43+
4044
.. _version-3.1.17:
4145

4246
3.1.17

djcelery/admin.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
PeriodicTask, IntervalSchedule, CrontabSchedule,
2626
)
2727
from .humanize import naturaldate
28-
from .utils import is_database_scheduler
28+
from .utils import is_database_scheduler, make_aware
2929

3030
try:
3131
from django.utils.encoding import force_text
@@ -68,13 +68,15 @@ def node_state(node):
6868
def eta(task):
6969
if not task.eta:
7070
return '<span style="color: gray;">none</span>'
71-
return escape(task.eta)
71+
return escape(make_aware(task.eta))
7272

7373

7474
@display_field(_('when'), 'tstamp')
7575
def tstamp(task):
76+
# convert to local timezone
77+
value = make_aware(task.tstamp)
7678
return '<div title="{0}">{1}</div>'.format(
77-
escape(str(task.tstamp)), escape(naturaldate(task.tstamp)),
79+
escape(str(value)), escape(naturaldate(value)),
7880
)
7981

8082

djcelery/humanize.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def naturaldate(date, include_seconds=False):
4848
delta_midnight = today - date
4949

5050
days = delta.days
51-
hours = int(round(delta.seconds / 3600, 0))
52-
minutes = delta.seconds / 60
51+
hours = delta.seconds // 3600
52+
minutes = delta.seconds // 60
5353
seconds = delta.seconds
5454

5555
if days < 0:
@@ -80,6 +80,6 @@ def naturaldate(date, include_seconds=False):
8080
count = 0
8181
for chunk, pluralizefun in OLDER_CHUNKS:
8282
if days >= chunk:
83-
count = round((delta_midnight.days + 1) / chunk, 0)
83+
count = int(round((delta_midnight.days + 1) / chunk, 0))
8484
fmt = pluralizefun(count)
8585
return fmt.format(num=count)

djcelery/loaders.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from celery.datastructures import DictAttribute
1212
from celery.loaders.base import BaseLoader
1313

14-
import django
1514
from django import db
1615
from django.conf import settings
1716
from django.core import cache
@@ -20,7 +19,6 @@
2019
from .utils import DATABASE_ERRORS, now
2120

2221
_RACE_PROTECTION = False
23-
NO_TZ = django.VERSION < (1, 4)
2422

2523

2624
def _maybe_close_fd(fh):
@@ -62,10 +60,6 @@ def read_configuration(self):
6260
getattr(settings, 'CELERY_BACKEND', None))
6361
if not backend:
6462
settings.CELERY_RESULT_BACKEND = 'database'
65-
if NO_TZ:
66-
if getattr(settings, 'CELERY_ENABLE_UTC', None):
67-
warn('CELERY_ENABLE_UTC requires Django 1.4+')
68-
settings.CELERY_ENABLE_UTC = False
6963
return DictAttribute(settings)
7064

7165
def _close_database(self):

djcelery/models.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from . import managers
1717
from .picklefield import PickledObjectField
18-
from .utils import fromtimestamp, now
18+
from .utils import now
1919
from .compat import python_2_unicode_compatible
2020

2121
ALL_STATES = sorted(states.ALL_STATES)
@@ -364,17 +364,6 @@ class Meta:
364364
get_latest_by = 'tstamp'
365365
ordering = ['-tstamp']
366366

367-
def save(self, *args, **kwargs):
368-
if self.eta is not None:
369-
self.eta = fromtimestamp(float('%d.%s' % (
370-
mktime(self.eta.timetuple()), self.eta.microsecond,
371-
)))
372-
if self.expires is not None:
373-
self.expires = fromtimestamp(float('%d.%s' % (
374-
mktime(self.expires.timetuple()), self.expires.microsecond,
375-
)))
376-
super(TaskState, self).save(*args, **kwargs)
377-
378367
def __str__(self):
379368
name = self.name or 'UNKNOWN'
380369
s = '{0.state:<10} {0.task_id:<36} {1}'.format(self, name)

djcelery/snapshot.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import absolute_import, unicode_literals
22

33
from collections import defaultdict
4-
from datetime import datetime, timedelta
4+
from datetime import timedelta
55

66
from django.conf import settings
77

@@ -13,7 +13,7 @@
1313
from celery.utils.timeutils import maybe_iso8601
1414

1515
from .models import WorkerState, TaskState
16-
from .utils import fromtimestamp, maybe_make_aware
16+
from .utils import fromtimestamp, correct_awareness
1717

1818
WORKER_UPDATE_FREQ = 60 # limit worker timestamp write freq.
1919
SUCCESS_STATES = frozenset([states.SUCCESS])
@@ -31,11 +31,6 @@
3131
debug = logger.debug
3232

3333

34-
def aware_tstamp(secs):
35-
"""Event timestamps uses the local timezone."""
36-
return maybe_make_aware(fromtimestamp(secs))
37-
38-
3934
class Camera(Polaroid):
4035
TaskState = TaskState
4136
WorkerState = WorkerState
@@ -57,10 +52,7 @@ def get_heartbeat(self, worker):
5752
heartbeat = worker.heartbeats[-1]
5853
except IndexError:
5954
return
60-
# Check for timezone settings
61-
if getattr(settings, "USE_TZ", False):
62-
return aware_tstamp(heartbeat)
63-
return datetime.fromtimestamp(heartbeat)
55+
return fromtimestamp(heartbeat)
6456

6557
def handle_worker(self, hostname_worker):
6658
(hostname, worker) = hostname_worker
@@ -86,10 +78,10 @@ def handle_task(self, uuid_task, worker=None):
8678
'name': task.name,
8779
'args': task.args,
8880
'kwargs': task.kwargs,
89-
'eta': maybe_make_aware(maybe_iso8601(task.eta)),
90-
'expires': maybe_make_aware(maybe_iso8601(task.expires)),
81+
'eta': correct_awareness(maybe_iso8601(task.eta)),
82+
'expires': correct_awareness(maybe_iso8601(task.expires)),
9183
'state': task.state,
92-
'tstamp': aware_tstamp(task.timestamp),
84+
'tstamp': fromtimestamp(task.timestamp),
9385
'result': task.result or task.exception,
9486
'traceback': task.traceback,
9587
'runtime': task.runtime,
@@ -121,9 +113,6 @@ def update_task(self, state, **kwargs):
121113

122114
for k, v in defaults.items():
123115
setattr(obj, k, v)
124-
for datefield in ('eta', 'expires', 'tstamp'):
125-
# Brute force trying to fix #183
126-
setattr(obj, datefield, maybe_make_aware(getattr(obj, datefield)))
127116
obj.save()
128117

129118
return obj

djcelery/tests/test_snapshot.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from celery.events.state import State, Worker, Task
1010
from celery.utils import gen_unique_id
1111

12+
from django.test.utils import override_settings
13+
from django.utils import timezone
14+
1215
from djcelery import celery
1316
from djcelery import snapshot
1417
from djcelery import models
@@ -74,7 +77,7 @@ def test_handle_worker(self):
7477

7578
def test_handle_task_received(self):
7679
worker = Worker(hostname='fuzzie')
77-
worker.event('oneline', time(), time(), {})
80+
worker.event('online', time(), time(), {})
7881
self.cam.handle_worker((worker.hostname, worker))
7982

8083
task = create_task(worker)
@@ -115,7 +118,60 @@ def test_handle_task(self):
115118
mt = self.cam.handle_task((task3.uuid, task3))
116119
self.assertIsNone(mt)
117120

121+
def test_handle_task_timezone(self):
122+
worker = Worker(hostname='fuzzie')
123+
worker.event('online', time(), time(), {})
124+
self.cam.handle_worker((worker.hostname, worker))
125+
126+
tstamp = 1464793200.0 # 2016-06-01T15:00:00Z
127+
128+
with override_settings(USE_TZ=True, TIME_ZONE='Europe/Helsinki'):
129+
task = create_task(worker,
130+
eta='2016-06-01T15:16:17.654321+00:00',
131+
expires='2016-07-01T15:16:17.765432+03:00')
132+
task.event('received', tstamp, tstamp, {})
133+
mt = self.cam.handle_task((task.uuid, task))
134+
self.assertEqual(
135+
mt.tstamp,
136+
datetime(2016, 6, 1, 15, 0, 0, tzinfo=timezone.utc),
137+
)
138+
self.assertEqual(
139+
mt.eta,
140+
datetime(2016, 6, 1, 15, 16, 17, 654321, tzinfo=timezone.utc),
141+
)
142+
self.assertEqual(
143+
mt.expires,
144+
datetime(2016, 7, 1, 12, 16, 17, 765432, tzinfo=timezone.utc),
145+
)
146+
147+
task = create_task(worker, eta='2016-06-04T15:16:17.654321')
148+
task.event('received', tstamp, tstamp, {})
149+
mt = self.cam.handle_task((task.uuid, task))
150+
self.assertEqual(
151+
mt.eta,
152+
datetime(2016, 6, 4, 15, 16, 17, 654321, tzinfo=timezone.utc),
153+
)
154+
155+
with override_settings(USE_TZ=False, TIME_ZONE='Europe/Helsinki'):
156+
task = create_task(worker,
157+
eta='2016-06-01T15:16:17.654321+00:00',
158+
expires='2016-07-01T15:16:17.765432+03:00')
159+
task.event('received', tstamp, tstamp, {})
160+
mt = self.cam.handle_task((task.uuid, task))
161+
self.assertEqual(mt.tstamp, datetime(2016, 6, 1, 18, 0, 0))
162+
self.assertEqual(mt.eta, datetime(2016, 6, 1, 18, 16, 17, 654321))
163+
self.assertEqual(mt.expires,
164+
datetime(2016, 7, 1, 15, 16, 17, 765432))
165+
166+
task = create_task(worker, eta='2016-06-04T15:16:17.654321')
167+
task.event('received', tstamp, tstamp, {})
168+
mt = self.cam.handle_task((task.uuid, task))
169+
self.assertEqual(mt.eta, datetime(2016, 6, 4, 15, 16, 17, 654321))
170+
118171
def assertExpires(self, dec, expired, tasks=10):
172+
# Cleanup leftovers from previous tests
173+
self.cam.on_cleanup()
174+
119175
worker = Worker(hostname='fuzzie')
120176
worker.event('online', time(), time(), {})
121177
for total in range(tasks):

djcelery/utils.py

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import datetime
66

77
from django.conf import settings
8+
from django.utils import timezone
89

910
# Database-related exceptions.
1011
from django.db import DatabaseError
@@ -44,51 +45,35 @@
4445
_oracle_database_errors)
4546

4647

47-
try:
48-
from django.utils import timezone
49-
is_aware = timezone.is_aware
50-
51-
# see Issue #222
52-
now_localtime = getattr(timezone, 'template_localtime', timezone.localtime)
53-
54-
def make_aware(value):
55-
if getattr(settings, 'USE_TZ', False):
56-
# naive datetimes are assumed to be in UTC.
57-
if timezone.is_naive(value):
58-
value = timezone.make_aware(value, timezone.utc)
59-
# then convert to the Django configured timezone.
60-
default_tz = timezone.get_default_timezone()
61-
value = timezone.localtime(value, default_tz)
62-
return value
63-
64-
def make_naive(value):
65-
if getattr(settings, 'USE_TZ', False):
66-
default_tz = timezone.get_default_timezone()
67-
value = timezone.make_naive(value, default_tz)
68-
return value
48+
def make_aware(value):
49+
if settings.USE_TZ:
50+
# naive datetimes are assumed to be in UTC.
51+
if timezone.is_naive(value):
52+
value = timezone.make_aware(value, timezone.utc)
53+
# then convert to the Django configured timezone.
54+
default_tz = timezone.get_default_timezone()
55+
value = timezone.localtime(value, default_tz)
56+
return value
6957

70-
def now():
71-
if getattr(settings, 'USE_TZ', False):
72-
return now_localtime(timezone.now())
73-
else:
74-
return timezone.now()
7558

76-
except ImportError:
77-
now = datetime.now
59+
def make_naive(value):
60+
if settings.USE_TZ:
61+
default_tz = timezone.get_default_timezone()
62+
value = timezone.make_naive(value, default_tz)
63+
return value
7864

79-
def _pass(x):
80-
return x
81-
make_aware = make_naive = _pass
8265

83-
def is_aware(x):
84-
return False
66+
def now():
67+
return make_aware(timezone.now())
8568

8669

87-
def maybe_make_aware(value):
88-
if isinstance(value, datetime) and is_aware(value):
89-
return value
90-
if value:
91-
return make_aware(value)
70+
def correct_awareness(value):
71+
if isinstance(value, datetime):
72+
if settings.USE_TZ:
73+
return make_aware(value)
74+
elif timezone.is_aware(value):
75+
default_tz = timezone.get_default_timezone()
76+
return timezone.make_naive(value, default_tz)
9277
return value
9378

9479

@@ -101,7 +86,7 @@ def is_database_scheduler(scheduler):
10186

10287

10388
def fromtimestamp(value):
104-
if getattr(settings, 'CELERY_ENABLE_UTC', False):
105-
return datetime.utcfromtimestamp(value)
89+
if settings.USE_TZ:
90+
return make_aware(datetime.utcfromtimestamp(value))
10691
else:
10792
return datetime.fromtimestamp(value)

0 commit comments

Comments
 (0)