Skip to content

Commit 2a9558c

Browse files
authored
Swallow and log any BaseException in ImmediateBackend other than KeyboardInterrupt (#81)
Fixes #50
1 parent 8c4ecc8 commit 2a9558c

File tree

4 files changed

+84
-20
lines changed

4 files changed

+84
-20
lines changed

django_tasks/backends/immediate.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,26 @@ def enqueue(
3737
try:
3838
result = json_normalize(calling_task_func(*args, **kwargs))
3939
status = ResultStatus.COMPLETE
40-
except Exception as e:
40+
except BaseException as e:
4141
try:
4242
result = exception_to_dict(e)
4343
except Exception:
4444
logger.exception("Task id=%s unable to save exception", result_id)
4545
result = None
46+
47+
# Use `.exception` to integrate with error monitoring tools (eg Sentry)
48+
logger.exception(
49+
"Task id=%s path=%s state=%s",
50+
result_id,
51+
task.module_path,
52+
ResultStatus.FAILED,
53+
)
4654
status = ResultStatus.FAILED
4755

56+
# If the user tried to terminate, let them
57+
if isinstance(e, KeyboardInterrupt):
58+
raise
59+
4860
task_result = TaskResult[T](
4961
task=task,
5062
id=result_id,

tests/tasks.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,18 @@ def calculate_meaning_of_life() -> int:
2222

2323

2424
@task()
25-
def failing_task() -> None:
26-
raise ValueError("This task failed")
25+
def failing_task_value_error() -> None:
26+
raise ValueError("This task failed due to ValueError")
27+
28+
29+
@task()
30+
def failing_task_system_exit() -> None:
31+
raise SystemExit("This task failed due to SystemExit")
32+
33+
34+
@task()
35+
def failing_task_keyboard_interrupt() -> None:
36+
raise KeyboardInterrupt("This task failed due to KeyboardInterrupt")
2737

2838

2939
@task()

tests/tests/test_database_backend.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def test_run_enqueued_task(self) -> None:
283283
def test_batch_processes_all_tasks(self) -> None:
284284
for _ in range(3):
285285
test_tasks.noop_task.enqueue()
286-
test_tasks.failing_task.enqueue()
286+
test_tasks.failing_task_value_error.enqueue()
287287

288288
self.assertEqual(DBTaskResult.objects.ready().count(), 4)
289289

@@ -329,7 +329,7 @@ def test_process_all_queues(self) -> None:
329329
self.assertEqual(DBTaskResult.objects.ready().count(), 0)
330330

331331
def test_failing_task(self) -> None:
332-
result = test_tasks.failing_task.enqueue()
332+
result = test_tasks.failing_task_value_error.enqueue()
333333
self.assertEqual(DBTaskResult.objects.ready().count(), 1)
334334

335335
with self.assertNumQueries(8):
@@ -346,7 +346,11 @@ def test_failing_task(self) -> None:
346346

347347
self.assertIsInstance(result.result, ValueError)
348348
assert result.traceback # So that mypy knows the next line is allowed
349-
self.assertTrue(result.traceback.endswith("ValueError: This task failed\n"))
349+
self.assertTrue(
350+
result.traceback.endswith(
351+
"ValueError: This task failed due to ValueError\n"
352+
)
353+
)
350354

351355
self.assertEqual(DBTaskResult.objects.ready().count(), 0)
352356

@@ -374,7 +378,7 @@ def test_complex_exception(self) -> None:
374378
self.assertEqual(DBTaskResult.objects.ready().count(), 0)
375379

376380
def test_doesnt_process_different_backend(self) -> None:
377-
result = test_tasks.failing_task.enqueue()
381+
result = test_tasks.failing_task_value_error.enqueue()
378382

379383
self.assertEqual(DBTaskResult.objects.ready().count(), 1)
380384

tests/tests/test_immediate_backend.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,57 @@ async def test_enqueue_task_async(self) -> None:
5050
self.assertEqual(result.kwargs, {})
5151

5252
def test_catches_exception(self) -> None:
53-
result = default_task_backend.enqueue(test_tasks.failing_task, [], {})
53+
test_data = [
54+
(
55+
test_tasks.failing_task_value_error, # task function
56+
ValueError, # expected exception
57+
"This task failed due to ValueError", # expected message
58+
),
59+
(
60+
test_tasks.failing_task_system_exit,
61+
SystemExit,
62+
"This task failed due to SystemExit",
63+
),
64+
]
65+
for task, exception, message in test_data:
66+
with self.subTest(task), self.assertLogs(
67+
"django_tasks.backends.immediate", level="ERROR"
68+
) as captured_logs:
69+
result = default_task_backend.enqueue(task, [], {})
70+
71+
# assert logging
72+
self.assertEqual(len(captured_logs.output), 1)
73+
self.assertIn(message, captured_logs.output[0])
74+
75+
# assert result
76+
self.assertEqual(result.status, ResultStatus.FAILED)
77+
self.assertIsNotNone(result.started_at)
78+
self.assertIsNotNone(result.finished_at)
79+
self.assertGreaterEqual(result.started_at, result.enqueued_at)
80+
self.assertGreaterEqual(result.finished_at, result.started_at)
81+
self.assertIsInstance(result.result, exception)
82+
self.assertTrue(
83+
result.traceback.endswith(f"{exception.__name__}: {message}\n")
84+
)
85+
self.assertIsNone(result.get_result())
86+
self.assertEqual(result.task, task)
87+
self.assertEqual(result.args, [])
88+
self.assertEqual(result.kwargs, {})
5489

55-
self.assertEqual(result.status, ResultStatus.FAILED)
56-
self.assertIsNotNone(result.started_at)
57-
self.assertIsNotNone(result.finished_at)
58-
self.assertGreaterEqual(result.started_at, result.enqueued_at)
59-
self.assertGreaterEqual(result.finished_at, result.started_at)
60-
self.assertIsInstance(result.result, ValueError)
61-
self.assertTrue(result.traceback.endswith("ValueError: This task failed\n"))
62-
self.assertIsNone(result.get_result())
63-
self.assertEqual(result.task, test_tasks.failing_task)
64-
self.assertEqual(result.args, [])
65-
self.assertEqual(result.kwargs, {})
90+
def test_throws_keyboard_interrupt(self) -> None:
91+
with self.assertRaises(KeyboardInterrupt):
92+
with self.assertLogs(
93+
"django_tasks.backends.immediate", level="ERROR"
94+
) as captured_logs:
95+
default_task_backend.enqueue(
96+
test_tasks.failing_task_keyboard_interrupt, [], {}
97+
)
98+
99+
# assert logging
100+
self.assertEqual(len(captured_logs.output), 1)
101+
self.assertIn(
102+
"This task failed due to KeyboardInterrupt", captured_logs.output[0]
103+
)
66104

67105
def test_complex_exception(self) -> None:
68106
with self.assertLogs("django_tasks.backends.immediate", level="ERROR"):
@@ -134,7 +172,7 @@ def test_cannot_pass_run_after(self) -> None:
134172
"Backend does not support run_after",
135173
):
136174
default_task_backend.validate_task(
137-
test_tasks.failing_task.using(run_after=timezone.now())
175+
test_tasks.failing_task_value_error.using(run_after=timezone.now())
138176
)
139177

140178
def test_meaning_of_life_view(self) -> None:

0 commit comments

Comments
 (0)