Skip to content

Commit 7abeb92

Browse files
authored
Merge pull request #15 from anexia/populate-db-error-handling
Fix infinite loop in populate_periodic_future_tasks on IntegrityError
2 parents 77ec58c + 42cb655 commit 7abeb92

File tree

2 files changed

+59
-9
lines changed

2 files changed

+59
-9
lines changed

django_future_tasks/management/commands/populate_periodic_future_tasks.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from croniter import croniter_range
66
from django import db
77
from django.core.management.base import BaseCommand
8+
from django.db import IntegrityError
89
from django.utils import timezone
910

1011
from django_future_tasks.models import FutureTask, PeriodicFutureTask
@@ -62,15 +63,21 @@ def handle_tick(self):
6263

6364
dt_format = "%Y-%m-%d %H:%M:%S%z"
6465
task_id = f"{p_task.periodic_task_id} ({dt.strftime(dt_format)})"
65-
FutureTask.objects.create(
66-
task_id=task_id,
67-
eta=dt,
68-
data=p_task.data,
69-
type=p_task.type,
70-
status=FutureTask.FUTURE_TASK_STATUS_OPEN,
71-
periodic_parent_task_id=p_task.pk,
72-
)
73-
logger.info(f"FutureTask {task_id} created")
66+
try:
67+
FutureTask.objects.create(
68+
task_id=task_id,
69+
eta=dt,
70+
data=p_task.data,
71+
type=p_task.type,
72+
status=FutureTask.FUTURE_TASK_STATUS_OPEN,
73+
periodic_parent_task_id=p_task.pk,
74+
)
75+
logger.info(f"FutureTask {task_id} created")
76+
except IntegrityError as exc:
77+
logger.warning(
78+
f"Database constraint violation creating FutureTask {task_id}: {exc}. Skipping duplicate task.",
79+
)
80+
continue
7481

7582
p_task.last_task_creation = now
7683
p_task.save()

tests/testapp/tests/test_periodic_future_tasks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import time
22
from datetime import timedelta
33

4+
import time_machine
45
from django.core.exceptions import ValidationError
56
from django.test import TransactionTestCase
67
from django.utils import timezone
@@ -137,3 +138,45 @@ def test_end_time_and_max_number_of_executions_validation(self):
137138
p_task.max_number_of_executions = 42
138139
p_task.end_time = timezone.now()
139140
self.assertRaises(ValidationError, p_task.save)
141+
142+
143+
class TestSkipPeriodicFutureTasks(TransactionTestCase):
144+
"""Test class for IntegrityError handling without background command threading"""
145+
146+
@time_machine.travel("2025-01-01 12:00:00+0000", tick=False)
147+
def test_populate_skips_duplicate_task_id_integrity_error(self):
148+
# Create periodic task that will generate tasks
149+
PeriodicFutureTask.objects.create(
150+
periodic_task_id="Fetch Data",
151+
type=settings.FUTURE_TASK_TYPE_ONE,
152+
cron_string="0 * * * *",
153+
last_task_creation=timezone.now() - timedelta(hours=2),
154+
)
155+
156+
# Create conflicting task that will cause IntegrityError
157+
conflicting_task_id = "Fetch Data (2025-01-01 12:00:00+0000)"
158+
FutureTask.objects.create(
159+
task_id=conflicting_task_id,
160+
eta=timezone.now(),
161+
type=settings.FUTURE_TASK_TYPE_ONE,
162+
)
163+
164+
initial_task_count = FutureTask.objects.count()
165+
166+
# Run populate command and verify IntegrityError handling
167+
from django_future_tasks.management.commands.populate_periodic_future_tasks import (
168+
Command,
169+
)
170+
171+
command = Command()
172+
command.tick = 1
173+
174+
with self.assertLogs(
175+
"populate_periodic_future_tasks",
176+
level="WARNING",
177+
) as log_capture:
178+
command.handle_tick()
179+
180+
# Verify warning was logged and task count unchanged
181+
self.assertIn("Skipping duplicate task", log_capture.records[0].getMessage())
182+
self.assertEqual(FutureTask.objects.count(), initial_task_count)

0 commit comments

Comments
 (0)