Skip to content

Commit e870812

Browse files
committed
Merge remote-tracking branch 'origin/master' into stopping-state
* origin/master: Quote all versions in ci.yml Correct support versions in README Remove Django 2.2 Add Django 4.0 and Python 3.10 to test matrix, remove unsupported versions Remove .python-version Reformat to match new Black version Blacken INSTALLED_APPS docs Bump version Blacken docs and reword slightly Clarify workspace docs Fix black formatting formatted w/black; added note about Windows support to README Test if `signal` has attribute `SIGQUIT` Add instructions for migrating
2 parents 631dc03 + d90e9fc commit e870812

12 files changed

+103
-41
lines changed

Diff for: .github/workflows/ci.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ jobs:
99

1010
strategy:
1111
matrix:
12-
python: [3.6, 3.7, 3.8, 3.9]
13-
django: [3.1, 3.2]
12+
python: ["3.6", "3.7", "3.8", "3.9", "3.10"]
13+
django: ["3.2", "4.0"]
14+
exclude:
15+
- python: "3.6"
16+
django: "4.0"
17+
- python: "3.7"
18+
django: "4.0"
1419
database_url:
1520
- postgres://runner:password@localhost/project
1621
- mysql://root:[email protected]/project

Diff for: .python-version

-1
This file was deleted.

Diff for: README.md

+67-29
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Simple database-backed job queue. Jobs are defined in your settings, and are pro
77
Asynchronous tasks are run via a *job queue*. This system is designed to support multi-step job workflows.
88

99
Supported and tested against:
10-
- Django 3.1 and 3.2
11-
- Python 3.6, 3.7, 3.8 and 3.9
10+
- Django 3.2 and 4.0
11+
- Python 3.6, 3.7, 3.8, 3.9 and 3.10
1212

1313
## Getting Started
1414

@@ -20,10 +20,16 @@ Install from PIP
2020

2121
Add `django_dbq` to your installed apps
2222

23-
INSTALLED_APPS = (
24-
...
25-
'django_dbq',
26-
)
23+
```python
24+
INSTALLED_APPS = [
25+
...,
26+
"django_dbq",
27+
]
28+
```
29+
30+
Run migrations
31+
32+
manage.py migrate
2733

2834
### Upgrading from 1.x to 2.x
2935

@@ -36,6 +42,7 @@ In e.g. project.common.jobs:
3642
```python
3743
import time
3844

45+
3946
def my_task(job):
4047
logger.info("Working hard...")
4148
time.sleep(10)
@@ -48,8 +55,8 @@ In project.settings:
4855

4956
```python
5057
JOBS = {
51-
'my_job': {
52-
'tasks': ['project.common.jobs.my_task']
58+
"my_job": {
59+
"tasks": ["project.common.jobs.my_task"],
5360
},
5461
}
5562
```
@@ -65,16 +72,16 @@ A failure hook receives the failed `Job` instance along with the unhandled excep
6572

6673
```python
6774
def my_task_failure_hook(job, e):
68-
# delete some temporary files on the filesystem
75+
... # clean up after failed job
6976
```
7077

7178
To ensure this hook gets run, simply add a `failure_hook` key to your job config like so:
7279

7380
```python
7481
JOBS = {
75-
'my_job': {
76-
'tasks': ['project.common.jobs.my_task'],
77-
'failure_hook': 'project.common.jobs.my_task_failure_hook'
82+
"my_job": {
83+
"tasks": ["project.common.jobs.my_task"],
84+
"failure_hook": "project.common.jobs.my_task_failure_hook",
7885
},
7986
}
8087
```
@@ -87,16 +94,16 @@ A creation hook receives your `Job` instance as its only argument. Here's an exa
8794

8895
```python
8996
def my_task_creation_hook(job):
90-
# configure something before running your job
97+
... # configure something before running your job
9198
```
9299

93100
To ensure this hook gets run, simply add a `creation_hook` key to your job config like so:
94101

95102
```python
96103
JOBS = {
97-
'my_job': {
98-
'tasks': ['project.common.jobs.my_task'],
99-
'creation_hook': 'project.common.jobs.my_task_creation_hook'
104+
"my_job": {
105+
"tasks": ["project.common.jobs.my_task"],
106+
"creation_hook": "project.common.jobs.my_task_creation_hook",
100107
},
101108
}
102109
```
@@ -112,7 +119,7 @@ In another terminal:
112119
Using the name you configured for your job in your settings, create an instance of Job.
113120

114121
```python
115-
Job.objects.create(name='my_job')
122+
Job.objects.create(name="my_job")
116123
```
117124

118125
### Prioritising jobs
@@ -121,10 +128,11 @@ important emails to users. However, once an hour, you may need to run a _really_
121128
of emails to be dispatched before it can begin.
122129

123130
In order to make sure that an important job is run before others, you can set the `priority` field to an integer higher than `0` (the default). For example:
131+
124132
```python
125-
Job.objects.create(name='normal_job')
126-
Job.objects.create(name='important_job', priority=1)
127-
Job.objects.create(name='critical_job', priority=2)
133+
Job.objects.create(name="normal_job")
134+
Job.objects.create(name="important_job", priority=1)
135+
Job.objects.create(name="critical_job", priority=2)
128136
```
129137

130138
Jobs will be ordered by their `priority` (highest to lowest) and then the time which they were created (oldest to newest) and processed in that order.
@@ -133,7 +141,10 @@ Jobs will be ordered by their `priority` (highest to lowest) and then the time w
133141
If you'd like to create a job but have it run at some time in the future, you can use the `run_after` field on the Job model:
134142

135143
```python
136-
Job.objects.create(name='scheduled_job', run_after=timezone.now() + timedelta(minutes=10))
144+
Job.objects.create(
145+
name="scheduled_job",
146+
run_after=(timezone.now() + timedelta(minutes=10)),
147+
)
137148
```
138149

139150
Of course, the scheduled job will only be run if your `python manage.py worker` process is running at the time when the job is scheduled to run. Otherwise, it will run the next time you start your worker process after that time has passed.
@@ -150,24 +161,47 @@ The top-level abstraction of a standalone piece of work. Jobs are stored in the
150161

151162
Jobs are processed to completion by *tasks*. These are simply Python functions, which must take a single argument - the `Job` instance being processed. A single job will often require processing by more than one task to be completed fully. Creating the task functions is the responsibility of the developer. For example:
152163

153-
def my_task(job):
154-
logger.info("Doing some hard work")
155-
do_some_hard_work()
164+
```python
165+
def my_task(job):
166+
logger.info("Doing some hard work")
167+
do_some_hard_work()
168+
```
156169

157170
### Workspace
158171

159-
The *workspace* is an area that tasks within a single job can use to communicate with each other. It is implemented as a Python dictionary, available on the `job` instance passed to tasks as `job.workspace`. The initial workspace of a job can be empty, or can contain some parameters that the tasks require (for example, API access tokens, account IDs etc). A single task can edit the workspace, and the modified workspace will be passed on to the next task in the sequence. For example:
172+
The *workspace* is an area that can be used 1) to provide additional arguments to task functions, and 2) to categorize jobs with additional metadata. It is implemented as a Python dictionary, available on the `job` instance passed to tasks as `job.workspace`. The initial workspace of a job can be empty, or can contain some parameters that the tasks require (for example, API access tokens, account IDs etc).
173+
174+
When creating a Job, the workspace is passed as a keyword argument:
175+
176+
```python
177+
Job.objects.create(name="my_job", workspace={"key": value})
178+
```
179+
180+
Then, the task function can access the workspace to get the data it needs to perform its task:
181+
182+
```python
183+
def my_task(job):
184+
cats_import = CatsImport.objects.get(pk=job.workspace["cats_import_id"])
185+
```
186+
187+
Tasks within a single job can use the workspace to communicate with each other. A single task can edit the workspace, and the modified workspace will be passed on to the next task in the sequence. For example:
160188

161189
def my_first_task(job):
162190
job.workspace['message'] = 'Hello, task 2!'
163191

164192
def my_second_task(job):
165193
logger.info("Task 1 says: %s" % job.workspace['message'])
166194

167-
When creating a Job, the workspace is passed as a keyword argument:
195+
The workspace can be queried like any [JSONField](https://docs.djangoproject.com/en/3.2/topics/db/queries/#querying-jsonfield). For instance, if you wanted to display a list of jobs that a certain user had initiated, add `user_id` to the workspace when creating the job:
168196

169197
```python
170-
Job.objects.create(name='my_job', workspace={'key': value})
198+
Job.objects.create(name="foo", workspace={"user_id": request.user.id})
199+
```
200+
201+
Then filter the query with it in the view that renders the list:
202+
203+
```python
204+
user_jobs = Job.objects.filter(workspace__user_id=request.user.id)
171205
```
172206

173207
### Worker process
@@ -208,8 +242,8 @@ from django_dbq.models import Job
208242

209243
...
210244

211-
Job.objects.create(name='do_work', workspace={})
212-
Job.objects.create(name='do_other_work', queue_name='other_queue', workspace={})
245+
Job.objects.create(name="do_work", workspace={})
246+
Job.objects.create(name="do_other_work", queue_name="other_queue", workspace={})
213247

214248
queue_depths = Job.get_queue_depths()
215249
print(queue_depths) # {"default": 1, "other_queue": 1}
@@ -244,6 +278,10 @@ jobs in the "NEW" or "READY" states will be returned.
244278

245279
It may be necessary to supply a DATABASE_PORT environment variable.
246280

281+
## Windows support
282+
283+
Windows is supported on a best-effort basis only, and is not covered by automated or manual testing.
284+
247285
## Code of conduct
248286

249287
For guidelines regarding the code of conduct when contributing to this repository please review [https://www.dabapps.com/open-source/code-of-conduct/](https://www.dabapps.com/open-source/code-of-conduct/)

Diff for: django_dbq/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.1.0"
1+
__version__ = "2.1.1"

Diff for: django_dbq/management/commands/queue_depth.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def handle(self, *args, **options):
1616
queue_depths_string = " ".join(
1717
[
1818
"{queue_name}={queue_depth}".format(
19-
queue_name=queue_name, queue_depth=queue_depths.get(queue_name, 0),
19+
queue_name=queue_name,
20+
queue_depth=queue_depths.get(queue_name, 0),
2021
)
2122
for queue_name in queue_names
2223
]

Diff for: django_dbq/management/commands/worker.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ def __init__(self, name, rate_limit_in_seconds):
2525

2626
def init_signals(self):
2727
signal.signal(signal.SIGINT, self.shutdown)
28-
signal.signal(signal.SIGQUIT, self.shutdown)
28+
29+
# for Windows, which doesn't support the SIGQUIT signal
30+
if hasattr(signal, "SIGQUIT"):
31+
signal.signal(signal.SIGQUIT, self.shutdown)
32+
2933
signal.signal(signal.SIGTERM, self.shutdown)
3034

3135
def shutdown(self, signum, frame):

Diff for: django_dbq/migrations/0001_initial.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class Migration(migrations.Migration):
4949
models.CharField(db_index=True, max_length=20, default="default"),
5050
),
5151
],
52-
options={"ordering": ["-created"],},
52+
options={
53+
"ordering": ["-created"],
54+
},
5355
),
5456
]

Diff for: django_dbq/migrations/0002_auto_20151016_1027.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ class Migration(migrations.Migration):
1111
]
1212

1313
operations = [
14-
migrations.AlterModelOptions(name="job", options={"ordering": ["created"]},),
14+
migrations.AlterModelOptions(
15+
name="job",
16+
options={"ordering": ["created"]},
17+
),
1518
]

Diff for: django_dbq/migrations/0003_auto_20180713_1000.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class Migration(migrations.Migration):
1313

1414
operations = [
1515
migrations.AlterModelOptions(
16-
name="job", options={"ordering": ["-priority", "created"]},
16+
name="job",
17+
options={"ordering": ["-priority", "created"]},
1718
),
1819
migrations.AddField(
1920
model_name="job",

Diff for: django_dbq/migrations/0004_auto_20210818_0247.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class Migration(migrations.Migration):
2727
),
2828
),
2929
migrations.AlterField(
30-
model_name="job", name="workspace", field=models.JSONField(null=True),
30+
model_name="job",
31+
name="workspace",
32+
field=models.JSONField(null=True),
3133
),
3234
]

Diff for: django_dbq/tests.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,14 @@ def test_queue_depth_multiple_queues(self):
103103
)
104104

105105
stdout = StringIO()
106-
call_command("queue_depth", queue_name=("default", "testqueue",), stdout=stdout)
106+
call_command(
107+
"queue_depth",
108+
queue_name=(
109+
"default",
110+
"testqueue",
111+
),
112+
stdout=stdout,
113+
)
107114
output = stdout.getvalue()
108115
self.assertEqual(output.strip(), "event=queue_depths default=2 testqueue=2")
109116

Diff for: test-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ freezegun==0.3.12
33
mock==3.0.5
44
dj-database-url==0.5.0
55
psycopg2==2.8.4
6-
black==19.10b0
6+
black==21.12b0

0 commit comments

Comments
 (0)