diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 99d98f8c..816520ff 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -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 @@ -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 @@ -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() diff --git a/t/unit/test_schedulers.py b/t/unit/test_schedulers.py index f7dfac8b..4b9d5031 100644 --- a/t/unit/test_schedulers.py +++ b/t/unit/test_schedulers.py @@ -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'), @@ -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( @@ -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( @@ -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'),