Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def calculate_meaning_of_life() -> int:

The task decorator accepts a few arguments to customize the task:

- `priority`: The priority of the task (larger numbers are higher priority)
- `priority`: The priority of the task (between -100 and 100. Larger numbers are higher priority. 0 by default)
- `queue_name`: Whether to run the task on a specific queue
- `backend`: Name of the backend for this task to use (as defined in `TASKS`)

Expand Down
12 changes: 9 additions & 3 deletions django_tasks/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing_extensions import ParamSpec

from django_tasks.exceptions import InvalidTaskError
from django_tasks.task import Task, TaskResult
from django_tasks.task import MAX_PRIORITY, MIN_PRIORITY, Task, TaskResult
from django_tasks.utils import is_global_function

T = TypeVar("T")
Expand Down Expand Up @@ -45,8 +45,14 @@ def validate_task(self, task: Task) -> None:
if not self.supports_async_task and iscoroutinefunction(task.func):
raise InvalidTaskError("Backend does not support async tasks")

if task.priority < 0:
raise InvalidTaskError("priority must be zero or greater")
if (
task.priority < MIN_PRIORITY
or task.priority > MAX_PRIORITY
or int(task.priority) != task.priority
):
raise InvalidTaskError(
f"priority must be a whole number between {MIN_PRIORITY} and {MAX_PRIORITY}"
)

if not self.supports_defer and task.run_after is not None:
raise InvalidTaskError("Backend does not support run_after")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-07-10 15:48

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("django_tasks_database", "0004_dbtaskresult_started_at"),
]

operations = [
migrations.AlterField(
model_name="dbtaskresult",
name="priority",
field=models.IntegerField(default=0),
),
migrations.AddConstraint(
model_name="dbtaskresult",
constraint=models.CheckConstraint(
check=models.Q(("priority__range", (-100, 100))), name="priority_range"
),
),
]
20 changes: 17 additions & 3 deletions django_tasks/backends/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@

from django.core.exceptions import SuspiciousOperation
from django.db import models
from django.db.models import F
from django.db.models import F, Q
from django.db.models.constraints import CheckConstraint
from django.utils import timezone
from django.utils.module_loading import import_string
from typing_extensions import ParamSpec

from django_tasks.task import DEFAULT_QUEUE_NAME, ResultStatus, Task
from django_tasks.task import (
DEFAULT_PRIORITY,
DEFAULT_QUEUE_NAME,
MAX_PRIORITY,
MIN_PRIORITY,
ResultStatus,
Task,
)
from django_tasks.utils import exception_to_dict, retry

logger = logging.getLogger("django_tasks.backends.database")
Expand Down Expand Up @@ -72,7 +80,7 @@ class DBTaskResult(GenericBase[P, T], models.Model):

args_kwargs = models.JSONField()

priority = models.PositiveSmallIntegerField(default=0)
priority = models.IntegerField(default=DEFAULT_PRIORITY)

task_path = models.TextField()

Expand All @@ -89,6 +97,12 @@ class Meta:
ordering = [F("priority").desc(), F("run_after").desc(nulls_last=True)]
verbose_name = "Task Result"
verbose_name_plural = "Task Results"
constraints = [
CheckConstraint(
check=Q(priority__range=(MIN_PRIORITY, MAX_PRIORITY)),
name="priority_range",
)
]

@property
def task(self) -> Task[P, T]:
Expand Down
8 changes: 6 additions & 2 deletions django_tasks/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

DEFAULT_TASK_BACKEND_ALIAS = "default"
DEFAULT_QUEUE_NAME = "default"
MIN_PRIORITY = -100
MAX_PRIORITY = 100
DEFAULT_PRIORITY = 0


class ResultStatus(TextChoices):
Expand Down Expand Up @@ -70,6 +73,7 @@ def name(self) -> str:

def using(
self,
*,
priority: Optional[int] = None,
queue_name: Optional[str] = None,
run_after: Optional[Union[datetime, timedelta]] = None,
Expand Down Expand Up @@ -163,7 +167,7 @@ def task(function: Callable[P, T], /) -> Task[P, T]: ...
@overload
def task(
*,
priority: int = 0,
priority: int = DEFAULT_PRIORITY,
queue_name: str = DEFAULT_QUEUE_NAME,
backend: str = DEFAULT_TASK_BACKEND_ALIAS,
) -> Callable[[Callable[P, T]], Task[P, T]]: ...
Expand All @@ -173,7 +177,7 @@ def task(
def task(
function: Optional[Callable[P, T]] = None,
*,
priority: int = 0,
priority: int = DEFAULT_PRIORITY,
queue_name: str = DEFAULT_QUEUE_NAME,
backend: str = DEFAULT_TASK_BACKEND_ALIAS,
) -> Union[Task[P, T], Callable[[Callable[P, T]], Task[P, T]]]:
Expand Down
36 changes: 34 additions & 2 deletions tests/tests/test_database_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from django.core.exceptions import SuspiciousOperation
from django.core.management import call_command, execute_from_command_line
from django.test import TestCase, TransactionTestCase, override_settings
from django.db.utils import IntegrityError
from django.test import TransactionTestCase, override_settings
from django.urls import reverse
from django.utils import timezone

Expand All @@ -29,7 +30,7 @@
}
}
)
class DatabaseBackendTestCase(TestCase):
class DatabaseBackendTestCase(TransactionTestCase):
def test_using_correct_backend(self) -> None:
self.assertEqual(default_task_backend, tasks["default"])
self.assertIsInstance(tasks["default"], DatabaseBackend)
Expand Down Expand Up @@ -204,6 +205,34 @@ def test_database_backend_app_missing(self) -> None:
self.assertEqual(len(errors), 1)
self.assertIn("django_tasks.backends.database", errors[0].hint)

def test_priority_range_check(self) -> None:
with self.assertRaises(IntegrityError):
DBTaskResult.objects.create(
task_path="", backend_name="default", priority=-101, args_kwargs={}
)

with self.assertRaises(IntegrityError):
DBTaskResult.objects.create(
task_path="", backend_name="default", priority=101, args_kwargs={}
)

# Django accepts the float, but only stores an int
result = DBTaskResult.objects.create(
task_path="", backend_name="default", priority=3.1, args_kwargs={}
)
result.refresh_from_db()
self.assertEqual(result.priority, 3)

DBTaskResult.objects.create(
task_path="", backend_name="default", priority=100, args_kwargs={}
)
DBTaskResult.objects.create(
task_path="", backend_name="default", priority=-100, args_kwargs={}
)
DBTaskResult.objects.create(
task_path="", backend_name="default", priority=0, args_kwargs={}
)


@override_settings(
TASKS={
Expand Down Expand Up @@ -426,6 +455,7 @@ def test_run_after_priority(self) -> None:
high_priority_result = test_tasks.noop_task.using(priority=10).enqueue()

low_priority_result = test_tasks.noop_task.using(priority=2).enqueue()
lower_priority_result = test_tasks.noop_task.using(priority=-2).enqueue()

self.assertEqual(
[dbt.task_result for dbt in DBTaskResult.objects.all()],
Expand All @@ -435,6 +465,7 @@ def test_run_after_priority(self) -> None:
low_priority_result,
far_future_result,
future_result,
lower_priority_result,
],
)

Expand All @@ -443,6 +474,7 @@ def test_run_after_priority(self) -> None:
[
high_priority_result,
low_priority_result,
lower_priority_result,
],
)

Expand Down
22 changes: 20 additions & 2 deletions tests/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
InvalidTaskError,
ResultDoesNotExist,
)
from django_tasks.task import MAX_PRIORITY, MIN_PRIORITY
from tests import tasks as test_tasks


Expand Down Expand Up @@ -133,9 +134,26 @@ def test_naive_datetime(self) -> None:

def test_invalid_priority(self) -> None:
with self.assertRaisesMessage(
InvalidTaskError, "priority must be zero or greater"
InvalidTaskError,
f"priority must be a whole number between {MIN_PRIORITY} and {MAX_PRIORITY}",
):
test_tasks.noop_task.using(priority=-1)
test_tasks.noop_task.using(priority=-101)

with self.assertRaisesMessage(
InvalidTaskError,
f"priority must be a whole number between {MIN_PRIORITY} and {MAX_PRIORITY}",
):
test_tasks.noop_task.using(priority=101)

with self.assertRaisesMessage(
InvalidTaskError,
f"priority must be a whole number between {MIN_PRIORITY} and {MAX_PRIORITY}",
):
test_tasks.noop_task.using(priority=3.1) # type:ignore[arg-type]

test_tasks.noop_task.using(priority=100)
test_tasks.noop_task.using(priority=-100)
test_tasks.noop_task.using(priority=0)

def test_call_task(self) -> None:
self.assertEqual(test_tasks.calculate_meaning_of_life.call(), 42)
Expand Down