Skip to content

Commit c454c65

Browse files
JinRiYao2001auvipy
andauthored
Fix the time calculation problem caused by start_time (#844)
* Fix the time calculation problem caused by start_time * Code Formatting * Modify the delay generation logic * created a method to calculate the exact delay time * Fix syntax errors * Changed due_start_time calculation logic and added unit test * Code formatting * formatting * Fixed time zone issue and added a test case * use zoneinfo * Force the time zone of start_time to be converted to tz * change test requirements * rollback requirements/test.txt * Update t/unit/test_schedulers.py * Adaptive py3.8 * Update t/unit/test_schedulers.py * Update django_celery_beat/schedulers.py * Update django_celery_beat/schedulers.py * Change the parameter name to solve the problem of too long code and solve the problem of test.txt dependency --------- Co-authored-by: Asif Saif Uddin <[email protected]>
1 parent ee2c505 commit c454c65

File tree

4 files changed

+147
-6
lines changed

4 files changed

+147
-6
lines changed

django_celery_beat/models.py

+11
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ def from_schedule(cls, schedule):
390390
except MultipleObjectsReturned:
391391
return cls.objects.filter(**spec).first()
392392

393+
def due_start_time(self, initial_start_time, tz):
394+
start_time = initial_start_time.astimezone(tz)
395+
start, ends_in, now = self.schedule.remaining_delta(start_time)
396+
return start + ends_in
397+
393398

394399
class PeriodicTasks(models.Model):
395400
"""Helper table for tracking updates to periodic tasks.
@@ -663,3 +668,9 @@ def scheduler(self):
663668
@property
664669
def schedule(self):
665670
return self.scheduler.schedule
671+
672+
def due_start_time(self, tz):
673+
if self.crontab:
674+
return self.crontab.due_start_time(self.start_time, tz)
675+
else:
676+
return self.start_time

django_celery_beat/schedulers.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ def is_due(self):
116116
if now < self.model.start_time:
117117
# The datetime is before the start date - don't run.
118118
# send a delay to retry on start_time
119-
delay = math.ceil(
120-
(self.model.start_time - now).total_seconds()
121-
)
119+
current_tz = now.tzinfo
120+
start_time = self.model.due_start_time(current_tz)
121+
time_remaining = start_time - now
122+
delay = math.ceil(time_remaining.total_seconds())
123+
122124
return schedules.schedstate(False, delay)
123125

124126
# EXPIRED TASK: Disable task when expired

requirements/test.txt

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
pytest-django>=4.5.2,<5.0
2-
pytest>=6.2.5,<9.0
3-
pytest-timeout
1+
# Base dependencies (common for all Python versions)
42
ephem
3+
pytest-timeout
4+
5+
# Conditional dependencies
6+
pytest>=6.2.5,<8.0; python_version < '3.9' # Python 3.8 only
7+
pytest>=6.2.5,<9.0; python_version >= '3.9' # Python 3.9+ only
8+
pytest-django>=4.5.2,<4.6.0; python_version < '3.9' # Python 3.8 only
9+
pytest-django>=4.5.2,<5.0; python_version >= '3.9' # Python 3.9+ only
10+
backports.zoneinfo; python_version < '3.9' # Python 3.8 only

t/unit/test_schedulers.py

+122
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from itertools import count
66
from time import monotonic
77

8+
try:
9+
from zoneinfo import ZoneInfo # Python 3.9+
10+
except ImportError:
11+
from backports.zoneinfo import ZoneInfo # Python 3.8
12+
813
import pytest
914
from celery.schedules import crontab, schedule, solar
1015
from django.contrib.admin.sites import AdminSite
@@ -776,6 +781,123 @@ def test_starttime_trigger(self, monkeypatch):
776781
assert s._heap[0]
777782
assert s._heap[0][2].name == m1.name
778783

784+
def test_crontab_with_start_time_between_now_and_crontab(self, app):
785+
now = app.now()
786+
delay_minutes = 2
787+
788+
test_start_time = now + timedelta(minutes=delay_minutes)
789+
790+
crontab_time = test_start_time + timedelta(minutes=delay_minutes)
791+
792+
task = self.create_model_crontab(
793+
crontab(minute=f'{crontab_time.minute}'),
794+
start_time=test_start_time)
795+
796+
entry = EntryTrackSave(task, app=app)
797+
798+
is_due, next_check = entry.is_due()
799+
800+
expected_delay = 2 * delay_minutes * 60
801+
802+
assert not is_due
803+
assert next_check == pytest.approx(expected_delay, abs=60)
804+
805+
def test_crontab_with_start_time_after_crontab(self, app):
806+
now = app.now()
807+
808+
delay_minutes = 2
809+
810+
crontab_time = now + timedelta(minutes=delay_minutes)
811+
812+
test_start_time = crontab_time + timedelta(minutes=delay_minutes)
813+
814+
task = self.create_model_crontab(
815+
crontab(minute=f'{crontab_time.minute}'),
816+
start_time=test_start_time)
817+
818+
entry = EntryTrackSave(task, app=app)
819+
820+
is_due, next_check = entry.is_due()
821+
822+
expected_delay = delay_minutes * 60 + 3600
823+
824+
assert not is_due
825+
assert next_check == pytest.approx(expected_delay, abs=60)
826+
827+
def test_crontab_with_start_time_different_time_zone(self, app):
828+
now = app.now()
829+
830+
delay_minutes = 2
831+
832+
test_start_time = now + timedelta(minutes=delay_minutes)
833+
834+
crontab_time = test_start_time + timedelta(minutes=delay_minutes)
835+
836+
tz = ZoneInfo('Asia/Shanghai')
837+
test_start_time = test_start_time.astimezone(tz)
838+
839+
task = self.create_model_crontab(
840+
crontab(minute=f'{crontab_time.minute}'),
841+
start_time=test_start_time)
842+
843+
entry = EntryTrackSave(task, app=app)
844+
845+
is_due, next_check = entry.is_due()
846+
847+
expected_delay = 2 * delay_minutes * 60
848+
849+
assert not is_due
850+
assert next_check == pytest.approx(expected_delay, abs=60)
851+
852+
now = app.now()
853+
854+
crontab_time = now + timedelta(minutes=delay_minutes)
855+
856+
test_start_time = crontab_time + timedelta(minutes=delay_minutes)
857+
858+
tz = ZoneInfo('Asia/Shanghai')
859+
test_start_time = test_start_time.astimezone(tz)
860+
861+
task = self.create_model_crontab(
862+
crontab(minute=f'{crontab_time.minute}'),
863+
start_time=test_start_time)
864+
865+
entry = EntryTrackSave(task, app=app)
866+
867+
is_due, next_check = entry.is_due()
868+
869+
expected_delay = delay_minutes * 60 + 3600
870+
871+
assert not is_due
872+
assert next_check == pytest.approx(expected_delay, abs=60)
873+
874+
def test_crontab_with_start_time_tick(self, app):
875+
PeriodicTask.objects.all().delete()
876+
s = self.Scheduler(app=self.app)
877+
assert not s._heap
878+
879+
m1 = self.create_model_interval(schedule(timedelta(seconds=3)))
880+
m1.save()
881+
882+
now = timezone.now()
883+
start_time = now + timedelta(minutes=1)
884+
crontab_trigger_time = now + timedelta(minutes=2)
885+
886+
m2 = self.create_model_crontab(
887+
crontab(minute=f'{crontab_trigger_time.minute}'),
888+
start_time=start_time)
889+
m2.save()
890+
891+
e2 = EntryTrackSave(m2, app=self.app)
892+
is_due, _ = e2.is_due()
893+
894+
max_iterations = 1000
895+
iterations = 0
896+
while (not is_due and iterations < max_iterations):
897+
s.tick()
898+
assert s._heap[0][2].name != m2.name
899+
is_due, _ = e2.is_due()
900+
779901

780902
@pytest.mark.django_db
781903
class test_models(SchedulerCase):

0 commit comments

Comments
 (0)