Skip to content

Commit f1e12d6

Browse files
committed
Merge branch 'master' into allow-runtime-settings-overrides
2 parents f86a29f + 23afd7d commit f1e12d6

21 files changed

+287
-94
lines changed

.circleci/config.yml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ references:
55
run:
66
name: Install Poetry
77
command: |
8-
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
8+
# Need to use version < 1.2.0 in order to support Python 3.6
9+
curl -sSL https://install.python-poetry.org | python3 - --version 1.1.15
910
restore-dependencies-cache: &restore-dependencies-cache
1011
restore_cache:
1112
keys:
@@ -14,7 +15,6 @@ references:
1415
run:
1516
name: Install Dependencies
1617
command: |
17-
source $HOME/.poetry/env
1818
poetry install
1919
poetry run pip install "django~=<< parameters.django-version >>.0"
2020
save-dependencies-cache: &save-dependencies-cache
@@ -65,7 +65,6 @@ jobs:
6565
- run:
6666
name: Run Tests
6767
command: |
68-
source $HOME/.poetry/env
6968
poetry run ./runtests
7069
7170
lint:
@@ -82,7 +81,6 @@ jobs:
8281
- run:
8382
name: Run Flake8
8483
command: |
85-
source $HOME/.poetry/env
8684
poetry run flake8
8785
8886
type-check:
@@ -99,7 +97,6 @@ jobs:
9997
- run:
10098
name: Run Mypy
10199
command: |
102-
source $HOME/.poetry/env
103100
poetry run ./script/type-check
104101
105102
deploy:
@@ -108,11 +105,10 @@ jobs:
108105
version: "3.7"
109106
steps:
110107
- checkout
108+
- *install-poetry
111109
- run:
112110
name: Push to PyPI
113111
command: |
114-
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
115-
source $HOME/.poetry/env
116112
poetry publish \
117113
--build \
118114
--username "${PYPI_USERNAME}" \

DEVELOPMENT.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Development
2+
3+
Dependencies are managed with [poetry](https://python-poetry.org/): `poetry install`
4+
5+
Tests are run with `./runtests`
6+
7+
## Releasing
8+
9+
CI handles releasing to PyPI.
10+
Releases on GitHub are created manually.
11+
12+
Here's how to do a release:
13+
14+
- Get all the desired changes into `master`
15+
- Wait for CI to pass that
16+
- Add a bump commit (see previous "Declare vX.Y.Z" commits; `poetry version` may be useful here)
17+
- Push that commit on master
18+
- Create a tag of that version number (`git tag v$(poetry version --short)`)
19+
- Push the tag (`git push --tags`)
20+
- CI will build & deploy that release
21+
- Create a Release on GitHub, ideally with a summary of changes

README.md

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ backends are great candidates for community contributions.
1515

1616
## Basic Usage
1717

18+
Start by adding `django_lightweight_queue` to your `INSTALLED_APPS`:
19+
20+
```python
21+
INSTALLED_APPS = [
22+
"django.contrib.admin",
23+
"django.contrib.auth",
24+
...,
25+
"django_lightweight_queue",
26+
]
27+
```
28+
29+
After that, define your task in any file you want:
30+
1831
```python
1932
import time
2033
from django_lightweight_queue import task
@@ -56,7 +69,7 @@ LIGHTWEIGHT_QUEUE_REDIS_PORT = 12345
5669
and then running:
5770

5871
```
59-
$ python manage.py queue_runner --config=special.py
72+
$ python manage.py queue_runner --extra-settings=special.py
6073
```
6174

6275
will result in the runner to use the settings from the specified configuration
@@ -67,12 +80,57 @@ present in the specified file are inherited from the global configuration.
6780

6881
There are four built-in backends:
6982

70-
| Backend | Type | Description |
71-
| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72-
| Synchronous | Development | Executes the task inline, without any actual queuing. |
73-
| Redis | Production | Executes tasks at-most-once using [Redis][redis] for storage of the enqueued tasks. |
74-
| Reliable Redis | Production | Executes tasks at-least-once using [Redis][redis] for storage of the enqueued tasks (subject to Redis consistency). Does not guarantee the task _completes_. |
75-
| Debug Web | Debugging | Instead of running jobs it prints the url to a view that can be used to run a task in a transaction which will be rolled back. This is useful for debugging and optimising tasks. |
83+
### Synchronous (Development backend)
84+
85+
`django_lightweight_queue.backends.synchronous.SynchronousBackend`
86+
87+
Executes the task inline, without any actual queuing.
88+
89+
### Redis (Production backend)
90+
91+
`django_lightweight_queue.backends.redis.RedisBackend`
92+
93+
Executes tasks at-most-once using [Redis][redis] for storage of the enqueued tasks.
94+
95+
### Reliable Redis (Production backend)
96+
97+
`django_lightweight_queue.backends.reliable_redis.ReliableRedisBackend`
98+
99+
Executes tasks at-least-once using [Redis][redis] for storage of the enqueued tasks (subject to Redis consistency). Does not guarantee the task _completes_.
100+
101+
### Debug Web (Debug backend)
102+
103+
`django_lightweight_queue.backends.debug_web.DebugWebBackend`
104+
105+
Instead of running jobs it prints the url to a view that can be used to run a task in a transaction which will be rolled back. This is useful for debugging and optimising tasks.
106+
107+
Use this to append the appropriate URLs to the bottom of your root `urls.py`:
108+
109+
```python
110+
from django.conf import settings
111+
from django.urls import path, include
112+
113+
urlpatterns = [
114+
...
115+
]
116+
117+
if settings.DEBUG:
118+
urlpatterns += [
119+
path(
120+
"",
121+
include(
122+
"django_lightweight_queue.urls", namespace="django-lightweight-queue"
123+
),
124+
)
125+
]
126+
```
127+
128+
This backend may require an extra setting if your debug site is not on localhost:
129+
130+
```python
131+
# defaults to http://localhost:8000
132+
LIGHTWEIGHT_QUEUE_SITE_URL = "http://example.com:8000"
133+
```
76134

77135
[redis]: https://redis.io/
78136

@@ -91,10 +149,13 @@ part of a pool:
91149
$ python manage.py queue_runner --machine 2 --of 4
92150
```
93151

94-
Alternatively a runner can be told explicitly which configuration to use:
152+
Alternatively a runner can be told explicitly how to behave by having
153+
extra settings loaded (any `LIGHTWEIGHT_QUEUE_*` constants found in the file
154+
will replace equivalent django settings) and being configured to run exactly as
155+
the settings describe:
95156

96157
```
97-
$ python manage.py queue_runner --exact-configuration --config=special.py
158+
$ python manage.py queue_runner --exact-configuration --extra-settings=special.py
98159
```
99160

100161
When using `--exact-configuration` the number of workers is configured exactly,
@@ -130,7 +191,7 @@ $ python manage.py queue_runner --machine 3 --of 3
130191
will result in one worker for `queue1` on the current machine, while:
131192

132193
```
133-
$ python manage.py queue_runner --exact-configuration --config=special.py
194+
$ python manage.py queue_runner --exact-configuration --extra-settings=special.py
134195
```
135196

136197
will result in two workers on the current machine.

django_lightweight_queue/cron_scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def get_matcher(minval, maxval, t):
144144
# No module, move on.
145145
continue
146146

147-
app_cron_config: List[CronConfig] = mod.CONFIG # type: ignore[attr-defined]
147+
app_cron_config: List[CronConfig] = mod.CONFIG
148148
for row in app_cron_config:
149149
row['min_matcher'] = get_matcher(0, 59, row.get('minutes'))
150150
row['hour_matcher'] = get_matcher(0, 23, row.get('hours'))

django_lightweight_queue/management/commands/queue_configuration.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
1+
import warnings
12
from typing import Any
23

34
from django.core.management.base import BaseCommand, CommandParser
45

5-
from ...utils import get_backend, get_queue_counts, load_extra_config
6+
from ...utils import get_backend, get_queue_counts, load_extra_settings
7+
from ...constants import SETTING_NAME_PREFIX
68
from ...app_settings import app_settings
79
from ...cron_scheduler import get_cron_config
810

911

1012
class Command(BaseCommand):
1113
def add_arguments(self, parser: CommandParser) -> None:
12-
parser.add_argument(
14+
extra_settings_group = parser.add_mutually_exclusive_group()
15+
extra_settings_group.add_argument(
1316
'--config',
1417
action='store',
1518
default=None,
16-
help="The path to an additional django-style config file to load",
19+
help="The path to an additional django-style config file to load "
20+
"(this spelling is deprecated in favour of '--extra-settings')",
21+
)
22+
extra_settings_group.add_argument(
23+
'--extra-settings',
24+
action='store',
25+
default=None,
26+
help="The path to an additional django-style settings file to load. "
27+
f"{SETTING_NAME_PREFIX}* settings discovered in this file will "
28+
"override those from the default Django settings.",
1729
)
1830

1931
def handle(self, **options: Any) -> None:
20-
# Configuration overrides
21-
extra_config = options['config']
32+
extra_config = options.pop('config')
2233
if extra_config is not None:
23-
load_extra_config(extra_config)
34+
warnings.warn(
35+
"Use of '--config' is deprecated in favour of '--extra-settings'.",
36+
category=DeprecationWarning,
37+
)
38+
options['extra_settings'] = extra_config
39+
40+
# Configuration overrides
41+
extra_settings = options['extra_settings']
42+
if extra_settings is not None:
43+
load_extra_settings(extra_settings)
2444

2545
print("django-lightweight-queue")
2646
print("========================")

django_lightweight_queue/management/commands/queue_runner.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import Any, Dict, Optional
23

34
import daemonize
@@ -10,8 +11,14 @@
1011
)
1112

1213
from ...types import QueueName
13-
from ...utils import get_logger, get_backend, get_middleware, load_extra_config
14+
from ...utils import (
15+
get_logger,
16+
get_backend,
17+
get_middleware,
18+
load_extra_settings,
19+
)
1420
from ...runner import runner
21+
from ...constants import SETTING_NAME_PREFIX
1522
from ...machine_types import Machine, PooledMachine, DirectlyConfiguredMachine
1623

1724

@@ -51,23 +58,42 @@ def add_arguments(self, parser: CommandParser) -> None:
5158
default=None,
5259
help="Only run the given queue, useful for local debugging",
5360
)
54-
parser.add_argument(
61+
extra_settings_group = parser.add_mutually_exclusive_group()
62+
extra_settings_group.add_argument(
5563
'--config',
5664
action='store',
5765
default=None,
58-
help="The path to an additional django-style config file to load",
66+
help="The path to an additional django-style config file to load "
67+
"(this spelling is deprecated in favour of '--extra-settings')",
68+
)
69+
extra_settings_group.add_argument(
70+
'--extra-settings',
71+
action='store',
72+
default=None,
73+
help="The path to an additional django-style settings file to load. "
74+
f"{SETTING_NAME_PREFIX}* settings discovered in this file will "
75+
"override those from the default Django settings.",
5976
)
6077
parser.add_argument(
6178
'--exact-configuration',
6279
action='store_true',
6380
help="Run queues on this machine exactly as specified. Requires the"
64-
" use of the '--config' option in addition. It is an error to"
65-
" use this option together with either '--machine' or '--of'.",
81+
" use of the '--extra-settings' option in addition. It is an"
82+
" error to use this option together with either '--machine' or"
83+
" '--of'.",
6684
)
6785

6886
def validate_and_normalise(self, options: Dict[str, Any]) -> None:
87+
extra_config = options.pop('config')
88+
if extra_config is not None:
89+
warnings.warn(
90+
"Use of '--config' is deprecated in favour of '--extra-settings'.",
91+
category=DeprecationWarning,
92+
)
93+
options['extra_settings'] = extra_config
94+
6995
if options['exact_configuration']:
70-
if not options['config']:
96+
if not options['extra_settings']:
7197
raise CommandError(
7298
"Must provide a value for '--config' when using "
7399
"'--exact-configuration'.",
@@ -110,9 +136,9 @@ def touch_filename(name: str) -> Optional[str]:
110136
return None
111137

112138
# Configuration overrides
113-
extra_config = options['config']
139+
extra_config = options['extra_settings']
114140
if extra_config is not None:
115-
load_extra_config(extra_config)
141+
load_extra_settings(extra_config)
116142

117143
logger.info("Starting queue master")
118144

django_lightweight_queue/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def runner(
4343

4444
# Note: we deliberately configure our handling of SIGTERM _after_ the
4545
# startup processes have happened; this ensures that the startup processes
46-
# (which could take a long time) are naturally interupted by the signal.
46+
# (which could take a long time) are naturally interrupted by the signal.
4747
def handle_term(signum: int, stack: object) -> None:
4848
nonlocal running
4949
logger.debug("Caught TERM signal")

django_lightweight_queue/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from django.conf.urls import url
1+
from django.urls import path
22

33
from . import views
44

55
app_name = 'django_lightweight_queue'
66

77
urlpatterns = (
8-
url(r'^debug/django-lightweight-queue/debug-run$', views.debug_run, name='debug-run'),
8+
path(r'debug/django-lightweight-queue/debug-run', views.debug_run, name='debug-run'),
99
)

django_lightweight_queue/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
FIVE_SECONDS = datetime.timedelta(seconds=5)
3434

3535

36-
def load_extra_config(file_path: str) -> None:
36+
def load_extra_settings(file_path: str) -> None:
3737
# Based on https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
3838
spec = importlib.util.spec_from_file_location('extra_settings', file_path)
3939
extra_settings = importlib.util.module_from_spec(spec) # type: ignore[arg-type]

django_lightweight_queue/worker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,25 @@ def configure_cancellation(self, timeout: Optional[int], sigkill_on_stop: bool)
159159
signal.signal(signal.SIGUSR2, self._handle_sigusr2)
160160

161161
if timeout is not None:
162+
signal.signal(signal.SIGALRM, self._handle_alarm)
162163
# alarm(3) takes whole seconds
163164
alarm_duration = int(math.ceil(timeout))
164165
signal.alarm(alarm_duration)
165166
else:
166167
# Cancel any scheduled alarms
167168
signal.alarm(0)
168169

170+
def _handle_alarm(self, signal_number: int, frame: object) -> None:
171+
# Log for observability
172+
self.log(logging.ERROR, "Alarm received: job has timed out")
173+
174+
# Disconnect ourselves then re-signal so that Python does what it
175+
# normally would. We could raise an exception here, however raising
176+
# exceptions from signal handlers is generally discouraged.
177+
signal.signal(signal.SIGALRM, signal.SIG_DFL)
178+
# TODO(python-upgrade): use signal.raise_signal on Python 3.8+
179+
os.kill(os.getpid(), signal.SIGALRM)
180+
169181
def set_process_title(self, *titles: str) -> None:
170182
set_process_title(self.name, *titles)
171183

0 commit comments

Comments
 (0)