Skip to content

Commit 6371b2b

Browse files
committed
Write documentation, update index name, add crontab tests
1 parent c58c9fa commit 6371b2b

File tree

5 files changed

+141
-8
lines changed

5 files changed

+141
-8
lines changed

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,83 @@
11
# django-dbtasks
22

33
Database backend and runner for [Django tasks](https://docs.djangoproject.com/en/dev/topics/tasks/) (new in 6.0).
4+
5+
6+
## Quickstart
7+
8+
Install the `django-dbtasks` package from PyPI, and configure your [TASKS setting](https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-TASKS) as follows:
9+
10+
```python
11+
TASKS = {
12+
"default": {
13+
"BACKEND": "dbtasks.backend.DatabaseBackend",
14+
"OPTIONS": {
15+
# Set this to True to execute tasks immediately (no need for a runner).
16+
"immediate": False,
17+
# How long to retain ScheduleTasks in the database. Forever if not set.
18+
"retain": datetime.timedelta(days=7),
19+
# Tasks to run periodically.
20+
"periodic": {
21+
# Runs at 3:30am every Monday through Friday.
22+
"myproject.tasks.maintenance": "30 3 * * 1-5",
23+
},
24+
},
25+
},
26+
}
27+
```
28+
29+
## Runner
30+
31+
`django-dbtasks` includes a dedicated `taskrunner` management command:
32+
33+
```
34+
usage: manage.py taskrunner [-h] [-w WORKERS] [-i WORKER_ID] [--backend BACKEND] [--delay DELAY]
35+
```
36+
37+
It is also straightforward to run the runner in a thread of its own:
38+
39+
```python
40+
runner = Runner(workers=4, worker_id="in-process")
41+
t = threading.Thread(target=runner.run)
42+
t.start()
43+
...
44+
runner.stop()
45+
t.join()
46+
```
47+
48+
`django-dbtasks` itself is tested on free-threading builds of Python 3.13 and 3.14, but compatibility will depend on your database driver and other packages.
49+
50+
51+
## Periodic Tasks
52+
53+
As shown in the [quickstart](#quickstart), periodic tasks are specified as a mapping in the backend `OPTIONS` under the `periodic` key. The keys of the mapping should be dotted paths to the tasks, and the values should either be a string in [crontab format](https://crontab.guru), or an instance of `dbtasks.Periodic`. Using a `dbtasks.Periodic` allows you to specify `args` and `kwargs` (as values or callables) that will be passed to the task. For example, the `Runner` registers a periodic task to remove old tasks past the retention window, in a manner similar to:
54+
55+
```python
56+
# Convert the `timedelta` to seconds, so as to be JSON-serializable.
57+
retain_secs = int(self.backend.options["retain"].total_seconds())
58+
# Clear old tasks every hour, on a random minute.
59+
self.periodic["dbtasks.runner.cleanup"] = Periodic("~ * * * *", args=[retain_secs])
60+
```
61+
62+
_Note that this allows you to specify a custom `Periodic` for the `dbtasks.runner.cleanup` task in your `TASKS` setting if necessary._
63+
64+
65+
## Logging
66+
67+
Be sure to add a `dbtasks` logger to your `LOGGING` setting:
68+
69+
```python
70+
LOGGING = {
71+
...
72+
"loggers": {
73+
"dbtasks": {
74+
"handlers": ["console"],
75+
"level": "INFO",
76+
},
77+
},
78+
}
79+
```
80+
81+
## Testing
82+
83+
There is a `RunnerTestCase` that starts a runner for the duration of a test suite. See [test_tasks.py](tests/tests/test_tasks.py) for example usage.
Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1+
import os
2+
import platform
3+
4+
from dbtasks.runner import Runner
15
from django.core.management import BaseCommand, CommandParser
26
from django.tasks import DEFAULT_TASK_BACKEND_ALIAS
37

4-
from dbtasks.runner import Runner
8+
9+
def cpus() -> int:
10+
return os.cpu_count() or 4
511

612

713
class Command(BaseCommand):
814
help = "Runs the task runner."
915

1016
def add_arguments(self, parser: CommandParser):
11-
parser.add_argument("--workers", default=4, type=int)
12-
parser.add_argument("--worker-id", default=None)
13-
parser.add_argument("--backend", default=DEFAULT_TASK_BACKEND_ALIAS)
17+
default_cpus = cpus() - 1
18+
default_node = platform.node()
19+
parser.add_argument(
20+
"-w",
21+
"--workers",
22+
type=int,
23+
default=default_cpus,
24+
help=f"Number of worker threads [default={default_cpus}]",
25+
)
26+
parser.add_argument(
27+
"-i",
28+
"--worker-id",
29+
default=None,
30+
help=f"Name of the worker node [default=`{default_node}`]",
31+
)
32+
parser.add_argument(
33+
"--backend",
34+
default=DEFAULT_TASK_BACKEND_ALIAS,
35+
help=f"Task backend to use [default=`{DEFAULT_TASK_BACKEND_ALIAS}`]",
36+
)
37+
parser.add_argument(
38+
"--delay",
39+
type=float,
40+
default=0.5,
41+
help="Loop delay [default=0.5]",
42+
)
1443

1544
def handle(self, *args, **options):
1645
Runner(
1746
workers=options["workers"],
1847
worker_id=options["worker_id"],
1948
backend=options["backend"],
49+
loop_delay=options["delay"],
2050
).run()

src/dbtasks/migrations/0001_initial.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Generated by Django 6.0 on 2025-12-20 02:16
22

3-
from django.db import migrations, models
4-
53
import dbtasks.models
4+
from django.db import migrations, models
65

76

87
class Migration(migrations.Migration):
@@ -59,7 +58,7 @@ class Migration(migrations.Migration):
5958
models.F("backend"),
6059
models.F("queue"),
6160
models.F("run_after"),
62-
name="idx_scheduledtask",
61+
name="idx_dbtasks_scheduledtask",
6362
)
6463
],
6564
},

src/dbtasks/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Meta:
6060
"backend",
6161
"queue",
6262
"run_after",
63-
name="idx_scheduledtask",
63+
name="idx_dbtasks_scheduledtask",
6464
),
6565
]
6666

tests/tests/test_crontab.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import unittest
2+
from datetime import datetime, time
3+
4+
from dbtasks.crontab import Crontab, hour, minute, month
5+
6+
7+
class CrontabTests(unittest.TestCase):
8+
def test_parts(self):
9+
self.assertEqual(minute.parse("30"), [30])
10+
self.assertEqual(minute.parse("7-12,42"), [7, 8, 9, 10, 11, 12, 42])
11+
self.assertEqual(len(minute.parse("~")), 1)
12+
self.assertEqual(minute.parse("*/15"), [0, 15, 30, 45])
13+
self.assertEqual(minute.parse("30-45/3"), [30, 33, 36, 39, 42, 45])
14+
self.assertEqual(hour.parse("*/23"), [0, 23])
15+
self.assertEqual(month.parse("oct,february,jun"), [2, 6, 10])
16+
self.assertEqual(month.parse("jan-jun/2"), [1, 3, 5])
17+
18+
def test_crontab(self):
19+
# 4:30am on the 1st and 15th of each month
20+
c = Crontab("30 4 1,15 * *")
21+
matches = list(c.dates(after=datetime(2025, 1, 1), until=datetime(2026, 1, 1)))
22+
self.assertEqual(len(matches), 12 * 2)
23+
self.assertTrue(all(d.day in (1, 15) for d in matches))
24+
self.assertTrue(all(d.time() == time(4, 30) for d in matches))

0 commit comments

Comments
 (0)