Skip to content

Commit 01a506c

Browse files
authored
Merge pull request #65 from james-certn/global-pre-job-hook
Add hook to run generic pre- and post-task logic
2 parents 9a8facd + 32b1ac9 commit 01a506c

File tree

6 files changed

+137
-14
lines changed

6 files changed

+137
-14
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,35 @@ JOBS = {
112112
}
113113
```
114114

115+
#### Pre & Post Task Hooks
116+
You can also run pre task or post task hooks, which happen in the normal processing of your `Job` instances and are executed inside the worker process.
117+
118+
Both pre and post task hooks receive your `Job` instance as their only argument. Here's an example:
119+
120+
```python
121+
def my_pre_task_hook(job):
122+
... # configure something before running your task
123+
```
124+
125+
To ensure these hooks are run, simply add a `pre_task_hook` or `post_task_hook` key (or both, if needed) to your job config like so:
126+
127+
```python
128+
JOBS = {
129+
"my_job": {
130+
"tasks": ["project.common.jobs.my_task"],
131+
"pre_task_hook": "project.common.jobs.my_pre_task_hook",
132+
"post_task_hook": "project.common.jobs.my_post_task_hook",
133+
},
134+
}
135+
```
136+
137+
Notes:
138+
139+
* If the `pre_task_hook` fails (raises an exception), the task function is not run, and django-db-queue behaves as if the task function itself had failed: the failure hook is called, and the job is goes into the `FAILED` state.
140+
* The `post_task_hook` is always run, even if the job fails. In this case, it runs after the `failure_hook`.
141+
* If the `post_task_hook` raises an exception, this is logged but the the job is **not marked as failed** and the failure hook does not run. This is because the `post_task_hook` might need to perform cleanup that always happens after the task, no matter whether it succeeds or fails.
142+
143+
115144
### Start the worker
116145

117146
In another terminal:

django_dbq/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.1.0"
1+
__version__ = "3.2.0"

django_dbq/management/commands/worker.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,23 @@ def _process_job(self):
7474
self.current_job = job
7575

7676
try:
77-
task_function = import_string(job.next_task)
78-
task_function(job)
77+
job.run_pre_task_hook()
78+
job.run_next_task()
7979
job.update_next_task()
80+
8081
if not job.next_task:
8182
job.state = Job.STATES.COMPLETE
8283
else:
8384
job.state = Job.STATES.READY
8485
except Exception as exception:
8586
logger.exception("Job id=%s failed", job.pk)
8687
job.state = Job.STATES.FAILED
87-
88-
failure_hook_name = job.get_failure_hook_name()
89-
if failure_hook_name:
90-
logger.info(
91-
"Running failure hook %s for job id=%s", failure_hook_name, job.pk
92-
)
93-
failure_hook_function = import_string(failure_hook_name)
94-
failure_hook_function(job, exception)
95-
else:
96-
logger.info("No failure hook for job id=%s", job.pk)
88+
job.run_failure_hook(exception)
89+
finally:
90+
try:
91+
job.run_post_task_hook()
92+
except:
93+
logger.exception("Job id=%s post_task_hook failed", job.pk)
9794

9895
logger.info(
9996
'Updating job: name="%s" id=%s state=%s next_task=%s',

django_dbq/models.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.utils.module_loading import import_string
44
from django_dbq.tasks import (
55
get_next_task_name,
6+
get_pre_task_hook_name,
7+
get_post_task_hook_name,
68
get_failure_hook_name,
79
get_creation_hook_name,
810
)
@@ -126,16 +128,47 @@ def save(self, *args, **kwargs):
126128
def update_next_task(self):
127129
self.next_task = get_next_task_name(self.name, self.next_task) or ""
128130

131+
def run_next_task(self):
132+
next_task_function = import_string(self.next_task)
133+
next_task_function(self)
134+
135+
def get_pre_task_hook_name(self):
136+
return get_pre_task_hook_name(self.name)
137+
138+
def get_post_task_hook_name(self):
139+
return get_post_task_hook_name(self.name)
140+
129141
def get_failure_hook_name(self):
130142
return get_failure_hook_name(self.name)
131143

132144
def get_creation_hook_name(self):
133145
return get_creation_hook_name(self.name)
134146

147+
def run_pre_task_hook(self):
148+
pre_task_hook_name = self.get_pre_task_hook_name()
149+
if pre_task_hook_name:
150+
logger.info("Running pre_task hook %s for job", pre_task_hook_name)
151+
pre_task_hook_function = import_string(pre_task_hook_name)
152+
pre_task_hook_function(self)
153+
154+
def run_post_task_hook(self):
155+
post_task_hook_name = self.get_post_task_hook_name()
156+
if post_task_hook_name:
157+
logger.info("Running post_task hook %s for job", post_task_hook_name)
158+
post_task_hook_function = import_string(post_task_hook_name)
159+
post_task_hook_function(self)
160+
161+
def run_failure_hook(self, exception):
162+
failure_hook_name = self.get_failure_hook_name()
163+
if failure_hook_name:
164+
logger.info("Running failure hook %s for job", failure_hook_name)
165+
failure_hook_function = import_string(failure_hook_name)
166+
failure_hook_function(self, exception)
167+
135168
def run_creation_hook(self):
136169
creation_hook_name = self.get_creation_hook_name()
137170
if creation_hook_name:
138-
logger.info("Running creation hook %s for new job", creation_hook_name)
171+
logger.info("Running creation hook %s for job", creation_hook_name)
139172
creation_hook_function = import_string(creation_hook_name)
140173
creation_hook_function(self)
141174

django_dbq/tasks.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33

44
TASK_LIST_KEY = "tasks"
5+
PRE_TASK_HOOK_KEY = "pre_task_hook"
6+
POST_TASK_HOOK_KEY = "post_task_hook"
57
FAILURE_HOOK_KEY = "failure_hook"
68
CREATION_HOOK_KEY = "creation_hook"
79

@@ -24,6 +26,16 @@ def get_next_task_name(job_name, current_task=None):
2426
return None
2527

2628

29+
def get_pre_task_hook_name(job_name):
30+
"""Return the name of the pre task hook for the given job (as a string) or None"""
31+
return settings.JOBS[job_name].get(PRE_TASK_HOOK_KEY)
32+
33+
34+
def get_post_task_hook_name(job_name):
35+
"""Return the name of the post_task hook for the given job (as a string) or None"""
36+
return settings.JOBS[job_name].get(POST_TASK_HOOK_KEY)
37+
38+
2739
def get_failure_hook_name(job_name):
2840
"""Return the name of the failure hook for the given job (as a string) or None"""
2941
return settings.JOBS[job_name].get(FAILURE_HOOK_KEY)

django_dbq/tests.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@ def failing_task(job):
3434
raise Exception("uh oh")
3535

3636

37+
def pre_task_hook(job):
38+
job.workspace["output"] = "pre task hook ran"
39+
job.workspace["job_id"] = str(job.id)
40+
41+
42+
def post_task_hook(job):
43+
job.workspace["output"] = "post task hook ran"
44+
job.workspace["job_id"] = str(job.id)
45+
46+
3747
def failure_hook(job, exception):
3848
job.workspace["output"] = "failure hook ran"
49+
job.workspace["exception"] = str(exception)
50+
job.workspace["job_id"] = str(job.id)
3951

4052

4153
def creation_hook(job):
4254
job.workspace["output"] = "creation hook ran"
55+
job.workspace["job_id"] = str(job.id)
4356

4457

4558
@override_settings(JOBS={"testjob": {"tasks": ["a"]}})
@@ -316,6 +329,7 @@ def test_creation_hook(self):
316329
job = Job.objects.create(name="testjob")
317330
job = Job.objects.get()
318331
self.assertEqual(job.workspace["output"], "creation hook ran")
332+
self.assertEqual(job.workspace["job_id"], str(job.id))
319333

320334
def test_creation_hook_only_runs_on_create(self):
321335
job = Job.objects.create(name="testjob")
@@ -326,6 +340,42 @@ def test_creation_hook_only_runs_on_create(self):
326340
self.assertEqual(job.workspace["output"], "creation hook output removed")
327341

328342

343+
@override_settings(
344+
JOBS={
345+
"testjob": {
346+
"tasks": ["django_dbq.tests.test_task"],
347+
"pre_task_hook": "django_dbq.tests.pre_task_hook",
348+
}
349+
}
350+
)
351+
class JobPreTaskHookTestCase(TestCase):
352+
def test_pre_task_hook(self):
353+
job = Job.objects.create(name="testjob")
354+
Worker("default", 1)._process_job()
355+
job = Job.objects.get()
356+
self.assertEqual(job.state, Job.STATES.COMPLETE)
357+
self.assertEqual(job.workspace["output"], "pre task hook ran")
358+
self.assertEqual(job.workspace["job_id"], str(job.id))
359+
360+
361+
@override_settings(
362+
JOBS={
363+
"testjob": {
364+
"tasks": ["django_dbq.tests.test_task"],
365+
"post_task_hook": "django_dbq.tests.post_task_hook",
366+
}
367+
}
368+
)
369+
class JobPostTaskHookTestCase(TestCase):
370+
def test_post_task_hook(self):
371+
job = Job.objects.create(name="testjob")
372+
Worker("default", 1)._process_job()
373+
job = Job.objects.get()
374+
self.assertEqual(job.state, Job.STATES.COMPLETE)
375+
self.assertEqual(job.workspace["output"], "post task hook ran")
376+
self.assertEqual(job.workspace["job_id"], str(job.id))
377+
378+
329379
@override_settings(
330380
JOBS={
331381
"testjob": {
@@ -341,6 +391,8 @@ def test_failure_hook(self):
341391
job = Job.objects.get()
342392
self.assertEqual(job.state, Job.STATES.FAILED)
343393
self.assertEqual(job.workspace["output"], "failure hook ran")
394+
self.assertIn("uh oh", job.workspace["exception"])
395+
self.assertEqual(job.workspace["job_id"], str(job.id))
344396

345397

346398
@override_settings(JOBS={"testjob": {"tasks": ["a"]}})

0 commit comments

Comments
 (0)