Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions django_celery_beat/schedulers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
from celery import current_app, schedules
from celery.beat import ScheduleEntry, Scheduler
from celery.utils.log import get_logger
from celery.utils.time import maybe_make_aware
from celery.utils.time import is_naive, make_aware
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import close_old_connections, transaction
from django.db.models import Case, F, IntegerField, Q, When
from django.db.models.functions import Cast
from django.db.utils import DatabaseError, InterfaceError
from django.utils import timezone
from kombu.utils.encoding import safe_repr, safe_str
from kombu.utils.json import dumps, loads

Expand Down Expand Up @@ -117,8 +118,6 @@ def is_due(self):
# START DATE: only run after the `start_time`, if one exists.
if self.model.start_time is not None:
now = self._default_now()
if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True):
now = maybe_make_aware(self._default_now())
if now < self.model.start_time:
# The datetime is before the start date - don't run.
# send a delay to retry on start_time
Expand Down Expand Up @@ -147,20 +146,27 @@ def is_due(self):
# Don't recheck
return schedules.schedstate(False, NEVER_CHECK_TIMEOUT)

# CAUTION: make_aware assumes settings.TIME_ZONE for naive datetimes,
# while maybe_make_aware assumes utc for naive datetimes
tz = self.app.timezone
last_run_at_in_tz = maybe_make_aware(self.last_run_at).astimezone(tz)
# Handle both naive and timezone-aware last_run_at properly
if is_naive(self.last_run_at):
# Naive datetime - make it aware using app timezone
last_run_at_in_tz = make_aware(self.last_run_at, self.app.timezone)
else:
# Already timezone-aware - convert to app timezone if needed
last_run_at_in_tz = self.last_run_at.astimezone(self.app.timezone)
return self.schedule.is_due(last_run_at_in_tz)

def _default_now(self):
if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True):
now = datetime.datetime.now(self.app.timezone)
else:
# this ends up getting passed to maybe_make_aware, which expects
# all naive datetime objects to be in utc time.
now = datetime.datetime.utcnow()
return now
now = timezone.now().astimezone(self.app.timezone)
tz_aware = (
getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and
getattr(settings, 'USE_TZ', True)
)
if tz_aware:
return now
# Return a naive datetime representing local time in the app's timezone.
# This path is taken when either DJANGO_CELERY_BEAT_TZ_AWARE or USE_TZ
# is set to False in Django settings.
return now.replace(tzinfo=None)

def __next__(self):
self.model.last_run_at = self._default_now()
Expand Down
8 changes: 4 additions & 4 deletions t/unit/test_schedulers.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def test_entry_is_due__no_use_tz(self):
assert self.app.timezone.key == 'Europe/Berlin'

# simulate last_run_at from DB - not TZ aware but localtime
right_now = datetime.utcnow()
right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None)

m = self.create_model_crontab(
crontab(minute='*/10'),
Expand Down Expand Up @@ -364,7 +364,7 @@ def test_entry_and_model_last_run_at_with_utc_no_use_tz(self, monkeypatch):
time.tzset()
assert self.app.timezone.key == 'Europe/Berlin'
# simulate last_run_at from DB - not TZ aware but localtime
right_now = datetime.utcnow()
right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None)
# make sure to use fixed date time
monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now)
m = self.create_model_crontab(
Expand Down Expand Up @@ -398,7 +398,7 @@ def test_entry_and_model_last_run_at_when_model_changed(self, monkeypatch):
time.tzset()
assert self.app.timezone.key == 'Europe/Berlin'
# simulate last_run_at from DB - not TZ aware but localtime
right_now = datetime.utcnow()
right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None)
# make sure to use fixed date time
monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now)
m = self.create_model_crontab(
Expand Down Expand Up @@ -451,7 +451,7 @@ def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self):

# simulate last_run_at all none, doing the same thing that
# _default_now() would do
right_now = datetime.utcnow()
right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None)

m = self.create_model_crontab(
crontab(minute='*/10'),
Expand Down