Skip to content

Commit 5fdf9fb

Browse files
committed
fix(Recurring Task): Prevent incorrect first run near midnight
1 parent 775362d commit 5fdf9fb

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

src/task_processor/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,23 @@ def should_execute(self) -> bool:
180180
# If we have never run this task, then we should execute it only if
181181
# the time has passed after which we want to ensure this task runs.
182182
# This allows us to control when intensive tasks should be run.
183-
return not (self.first_run_time and self.first_run_time > now.time())
183+
if not self.first_run_time:
184+
return True
185+
first_run_today = now.replace(
186+
hour=self.first_run_time.hour,
187+
minute=self.first_run_time.minute,
188+
second=self.first_run_time.second,
189+
microsecond=self.first_run_time.microsecond,
190+
)
191+
# Handle midnight boundary using 12-hour window heuristic.
192+
time_difference = (now - first_run_today).total_seconds()
193+
if time_difference > 12 * 3600:
194+
# first_run_today appears far in the past; it refers to tomorrow.
195+
return False
196+
if time_difference < -12 * 3600:
197+
# first_run_today appears far in the future; it refers to yesterday.
198+
return True
199+
return now >= first_run_today
184200

185201
# if the last run was at t- run_every, then we should execute it
186202
if (timezone.now() - last_task_run.started_at) >= self.run_every:

tests/unit/task_processor/test_unit_task_processor_models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import pytest
55
from django.utils import timezone
6+
from freezegun import freeze_time
67
from pytest_mock import MockerFixture
78

89
from task_processor.decorators import register_task_handler
@@ -80,3 +81,31 @@ def test_recurring_task_run_should_execute_first_run_at(
8081
).should_execute
8182
== expected
8283
)
84+
85+
86+
@freeze_time("2026-01-15 23:05:23")
87+
def test_recurring_task_should_execute__first_run_time_after_midnight__returns_false() -> (
88+
None
89+
):
90+
# Given
91+
task = RecurringTask(
92+
first_run_time=time(0, 5, 23),
93+
run_every=timedelta(days=1),
94+
)
95+
96+
# When & Then
97+
assert task.should_execute is False
98+
99+
100+
@freeze_time("2026-01-16 00:30:00")
101+
def test_recurring_task_should_execute__first_run_time_before_midnight__returns_true() -> (
102+
None
103+
):
104+
# Given
105+
task = RecurringTask(
106+
first_run_time=time(23, 0, 0),
107+
run_every=timedelta(days=1),
108+
)
109+
110+
# When & Then
111+
assert task.should_execute is True

0 commit comments

Comments
 (0)