Skip to content

Commit 0cef80a

Browse files
marky1991auvipy
authored andcommitted
Fix case where last_run_at=None and CELERY_TIMEZONE!=TIME_ZONE (#294)
* Fix case where last_run_at=None, USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False, and CELERY_TIMEZONE != settings.TIME_ZONE. * Make _default_now always return a naive datetime in utc when returning a naive datetime. maybe_make_aware expects naive objects to be in utc time, while make_aware expects localtime. Here, we swap to matching maybe_make_aware's expectations and call it instead. It would be better if the two functions expected the same thing, but I'm not sure how disruptive that change would be. Also, I updated a few gaps in the relevant tests and made them more real. (resetting TZ like django would for one) * Fix case where last_run_at=None, USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False, and CELERY_TIMEZONE != settings.TIME_ZONE. * Make _default_now always return a naive datetime in utc when returning a naive datetime. maybe_make_aware expects naive objects to be in utc time, while make_aware expects localtime. Here, we swap to matching maybe_make_aware's expectations and call it instead. It would be better if the two functions expected the same thing, but I'm not sure how disruptive that change would be. Also, I updated a few gaps in the relevant tests and made them more real. (resetting TZ like django would for one) * Fix flake8 errors. * Fix flake issues in this file too. I only changed these two so hopefully that's it. * Fix readme.md I think we are hitting a bug in the markdown processing, where triple backticks are not working properly... I got it to work another way here though, so saving this to make the readme show up properly. * Fix case where last_run_at=None, USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False, and CELERY_TIMEZONE != settings.TIME_ZONE. * Make _default_now always return a naive datetime in utc when returning a naive datetime. maybe_make_aware expects naive objects to be in utc time, while make_aware expects localtime. Here, we swap to matching maybe_make_aware's expectations and call it instead. It would be better if the two functions expected the same thing, but I'm not sure how disruptive that change would be. Also, I updated a few gaps in the relevant tests and made them more real. (resetting TZ like django would for one) * Fix flake8 errors. * Fix flake issues in this file too. I only changed these two so hopefully that's it.
1 parent 3b16c5e commit 0cef80a

File tree

2 files changed

+56
-4
lines changed

2 files changed

+56
-4
lines changed

django_celery_beat/schedulers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
CrontabSchedule, IntervalSchedule,
2727
SolarSchedule, ClockedSchedule
2828
)
29-
from .utils import make_aware
3029
from .clockedschedule import clocked
3130

3231
try:
@@ -134,7 +133,11 @@ def is_due(self):
134133
self.model.save()
135134
return schedules.schedstate(False, None) # Don't recheck
136135

137-
return self.schedule.is_due(make_aware(self.last_run_at))
136+
# CAUTION: make_aware assumes settings.TIME_ZONE for naive datetimes,
137+
# while maybe_make_aware assumes utc for naive datetimes
138+
tz = self.app.timezone
139+
last_run_at_in_tz = maybe_make_aware(self.last_run_at).astimezone(tz)
140+
return self.schedule.is_due(last_run_at_in_tz)
138141

139142
def _default_now(self):
140143
# The PyTZ datetime must be localised for the Django-Celery-Beat
@@ -144,7 +147,9 @@ def _default_now(self):
144147
now = self.app.now()
145148
now = now.tzinfo.localize(now.replace(tzinfo=None))
146149
else:
147-
now = datetime.datetime.now()
150+
# this ends up getting passed to maybe_make_aware, which expects
151+
# all naive datetime objects to be in utc time.
152+
now = datetime.datetime.utcnow()
148153
return now
149154

150155
def __next__(self):

t/unit/test_schedulers.py

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

33
import math
4+
import os
45
import time
56
import pytest
67

@@ -151,13 +152,18 @@ def test_entry(self):
151152
USE_TZ=False,
152153
DJANGO_CELERY_BEAT_TZ_AWARE=False
153154
)
155+
@pytest.mark.usefixtures('depends_on_current_app')
154156
@timezone.override('Europe/Berlin')
155157
@pytest.mark.celery(timezone='Europe/Berlin')
156158
def test_entry_is_due__no_use_tz(self):
159+
old_tz = os.environ.get("TZ")
160+
os.environ["TZ"] = "Europe/Berlin"
161+
if hasattr(time, "tzset"):
162+
time.tzset()
157163
assert self.app.timezone.zone == 'Europe/Berlin'
158164

159165
# simulate last_run_at from DB - not TZ aware but localtime
160-
right_now = timezone.now()
166+
right_now = datetime.utcnow()
161167

162168
m = self.create_model_crontab(
163169
crontab(minute='*/10'),
@@ -167,6 +173,47 @@ def test_entry_is_due__no_use_tz(self):
167173

168174
assert e.is_due().is_due is False
169175
assert e.is_due().next <= 600 # 10 minutes; see above
176+
if old_tz is not None:
177+
os.environ["TZ"] = old_tz
178+
else:
179+
del os.environ["TZ"]
180+
if hasattr(time, "tzset"):
181+
time.tzset()
182+
183+
@override_settings(
184+
USE_TZ=False,
185+
DJANGO_CELERY_BEAT_TZ_AWARE=False,
186+
TIME_ZONE="Europe/Berlin",
187+
CELERY_TIMEZONE="America/New_York"
188+
)
189+
@pytest.mark.usefixtures('depends_on_current_app')
190+
@timezone.override('Europe/Berlin')
191+
@pytest.mark.celery(timezone='America/New_York')
192+
def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self):
193+
old_tz = os.environ.get("TZ")
194+
os.environ["TZ"] = "Europe/Berlin"
195+
if hasattr(time, "tzset"):
196+
time.tzset()
197+
assert self.app.timezone.zone == 'America/New_York'
198+
199+
# simulate last_run_at all none, doing the same thing that
200+
# _default_now() would do
201+
right_now = datetime.utcnow()
202+
203+
m = self.create_model_crontab(
204+
crontab(minute='*/10'),
205+
last_run_at=right_now,
206+
)
207+
e = self.Entry(m, app=self.app)
208+
209+
assert e.is_due().is_due is False
210+
assert e.is_due().next <= 600 # 10 minutes; see above
211+
if old_tz is not None:
212+
os.environ["TZ"] = old_tz
213+
else:
214+
del os.environ["TZ"]
215+
if hasattr(time, "tzset"):
216+
time.tzset()
170217

171218
def test_task_with_start_time(self):
172219
interval = 10

0 commit comments

Comments
 (0)