Skip to content

Commit c26bfc5

Browse files
Add additional test cases for process management (#88)
Sending signals on Windows is surprisingly difficult. CTRL-C is handled by `SIGINT`, which is tested on other platforms and should behave the same. Improving test coverage for Windows will be a future task.
1 parent 00b54c4 commit c26bfc5

File tree

11 files changed

+305
-22
lines changed

11 files changed

+305
-22
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ jobs:
4949
run: just lint
5050
- name: Run tests
5151
run: just test
52+
- name: Run fast tests
53+
run: just test-fast
5254

5355
test-postgres:
5456
runs-on: ubuntu-latest

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ cover/
6262
# Django stuff:
6363
*.log
6464
local_settings.py
65-
db.sqlite3
66-
db.sqlite3-journal
65+
*.sqlite3
66+
*.sqlite3-journal
6767

6868
# Flask stuff:
6969
instance/

CONTRIBUTING.md

+6
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,9 @@ just test-sqlite
4242
# To run all of the above:
4343
just test-dbs
4444
```
45+
46+
Due to database worker process' tests, tests cannot run using an in-memory database, which means tests run quite slow locally. If you're not modifying the worker, and want you tests run run quicker, run:
47+
48+
```sh
49+
just test-fast
50+
```

django_tasks/backends/database/management/commands/db_worker.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,28 @@
2727

2828
class Worker:
2929
def __init__(
30-
self, *, queue_names: List[str], interval: float, batch: bool, backend_name: str
30+
self,
31+
*,
32+
queue_names: List[str],
33+
interval: float,
34+
batch: bool,
35+
backend_name: str,
36+
startup_delay: bool,
3137
):
3238
self.queue_names = queue_names
3339
self.process_all_queues = "*" in queue_names
3440
self.interval = interval
3541
self.batch = batch
3642
self.backend_name = backend_name
43+
self.startup_delay = startup_delay
3744

3845
self.running = True
3946
self.running_task = False
4047

4148
def shutdown(self, signum: int, frame: Optional[FrameType]) -> None:
4249
if not self.running:
4350
logger.warning(
44-
"Received %s - shutting down immediately.", signal.strsignal(signum)
51+
"Received %s - terminating current task.", signal.strsignal(signum)
4552
)
4653
sys.exit(1)
4754

@@ -67,7 +74,7 @@ def start(self) -> None:
6774

6875
logger.info("Starting worker for queues=%s", ",".join(self.queue_names))
6976

70-
if self.interval:
77+
if self.startup_delay and self.interval:
7178
# Add a random small delay before starting the loop to avoid a thundering herd
7279
time.sleep(random.random())
7380

@@ -87,7 +94,7 @@ def start(self) -> None:
8794
except OperationalError as e:
8895
# Ignore locked databases and keep trying.
8996
# It should unlock eventually.
90-
if "database is locked" in e.args[0]:
97+
if "is locked" in e.args[0]:
9198
task_result = None
9299
else:
93100
raise
@@ -150,10 +157,6 @@ def run_task(self, db_task_result: DBTaskResult) -> None:
150157
task_result=task_result,
151158
)
152159

153-
# If the user tried to terminate, let them
154-
if isinstance(e, KeyboardInterrupt):
155-
raise
156-
157160

158161
def valid_backend_name(val: str) -> str:
159162
try:
@@ -205,6 +208,12 @@ def add_arguments(self, parser: ArgumentParser) -> None:
205208
dest="backend_name",
206209
help="The backend to operate on (default: %(default)r)",
207210
)
211+
parser.add_argument(
212+
"--no-startup-delay",
213+
action="store_false",
214+
dest="startup_delay",
215+
help="Don't add a small delay at startup.",
216+
)
208217

209218
def configure_logging(self, verbosity: int) -> None:
210219
if verbosity == 0:
@@ -229,6 +238,7 @@ def handle(
229238
interval: float,
230239
batch: bool,
231240
backend_name: str,
241+
startup_delay: bool,
232242
**options: dict,
233243
) -> None:
234244
self.configure_logging(verbosity)
@@ -238,6 +248,7 @@ def handle(
238248
interval=interval,
239249
batch=batch,
240250
backend_name=backend_name,
251+
startup_delay=startup_delay,
241252
)
242253

243254
worker.start()

justfile

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ test *ARGS:
99
python -m coverage report
1010
python -m coverage html
1111

12+
test-fast *ARGS:
13+
python -m manage test --shuffle --noinput --settings tests.settings_fast {{ ARGS }}
14+
1215
format:
1316
python -m ruff check django_tasks tests --fix
1417
python -m ruff format django_tasks tests
@@ -19,7 +22,6 @@ lint:
1922
python -m mypy django_tasks tests
2023

2124
start-dbs:
22-
docker-compose pull
2325
docker-compose up -d
2426

2527
test-sqlite *ARGS:

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ postgres = [
6565
select = ["E", "F", "I", "W", "N", "B", "A", "C4", "T20", "DJ"]
6666
ignore = ["E501", "DJ008"]
6767

68+
[tool.ruff.lint.per-file-ignores]
69+
"tests/db_worker_test_settings.py" = ["F403", "F405"]
70+
"tests/settings_fast.py" = ["F403", "F405"]
71+
6872
[tool.mypy]
6973
plugins = ["mypy_django_plugin.main"]
7074
warn_unused_ignores = true

tests/db_worker_test_settings.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .settings import *
2+
3+
TASKS = {"default": {"BACKEND": "django_tasks.backends.database.DatabaseBackend"}}
4+
5+
# Force the test DB to be used
6+
if "sqlite" in DATABASES["default"]["ENGINE"]:
7+
DATABASES["default"]["NAME"] = DATABASES["default"]["TEST"]["NAME"]
8+
else:
9+
DATABASES["default"]["NAME"] = "test_" + DATABASES["default"]["NAME"]

tests/settings.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,18 @@
5555

5656
DATABASES = {
5757
"default": dj_database_url.config(
58-
default="sqlite://:memory:"
59-
if IN_TEST
60-
else "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
58+
default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
6159
)
6260
}
6361

6462
# Set exclusive transactions in 5.1+
6563
if django.VERSION >= (5, 1) and "sqlite" in DATABASES["default"]["ENGINE"]:
6664
DATABASES["default"].setdefault("OPTIONS", {})["transaction_mode"] = "EXCLUSIVE"
6765

66+
if "sqlite" in DATABASES["default"]["ENGINE"]:
67+
DATABASES["default"]["TEST"] = {"NAME": os.path.join(BASE_DIR, "db-test.sqlite3")}
68+
69+
6870
USE_TZ = True
6971

7072
if not IN_TEST:

tests/settings_fast.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .settings import *
2+
3+
# Unset custom test settings to use in-memory DB
4+
if "sqlite" in DATABASES["default"]["ENGINE"]:
5+
del DATABASES["default"]["TEST"]

tests/tasks.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
from django_tasks import task
24

35

@@ -54,3 +56,16 @@ def enqueue_on_commit_task() -> None:
5456
@task(enqueue_on_commit=False)
5557
def never_enqueue_on_commit_task() -> None:
5658
pass
59+
60+
61+
@task()
62+
def hang() -> None:
63+
"""
64+
Do nothing for 5 minutes
65+
"""
66+
time.sleep(300)
67+
68+
69+
@task()
70+
def sleep_for(seconds: float) -> None:
71+
time.sleep(seconds)

0 commit comments

Comments
 (0)