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 <auvipy@gmail.com>
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 5 additions & 3 deletions
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

Lines changed: 9 additions & 3 deletions
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

Lines changed: 122 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy