Skip to content

Commit 602a073

Browse files
committed
Improve auto reload for cqrs_consume
1 parent 72c48b0 commit 602a073

File tree

6 files changed

+284
-63
lines changed

6 files changed

+284
-63
lines changed

dj_cqrs/management/commands/cqrs_consume.py

Lines changed: 142 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,116 @@
11
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
2-
import multiprocessing
2+
import logging
33
import signal
4+
import threading
5+
from pathlib import Path
46

57
from django.core.management.base import BaseCommand, CommandError
8+
from watchfiles import watch
9+
from watchfiles.filters import PythonFilter
10+
from watchfiles.run import start_process
611

712
from dj_cqrs.registries import ReplicaRegistry
8-
from dj_cqrs.transport import current_transport
13+
14+
15+
logger = logging.getLogger('django_cqrs.cqrs_consume')
16+
17+
18+
def consume(**kwargs):
19+
import django
20+
django.setup()
21+
22+
from dj_cqrs.transport import current_transport
23+
current_transport.consume(**kwargs)
924

1025

1126
class WorkersManager:
1227

13-
def __init__(self, options, transport, consume_kwargs):
28+
def __init__(
29+
self,
30+
consume_kwargs,
31+
workers=1,
32+
reload=False,
33+
ignore_paths=None,
34+
sigint_timeout=5,
35+
sigkill_timeout=1,
36+
):
1437
self.pool = []
15-
self.options = options
16-
self.transport = transport
38+
self.workers = workers
39+
self.reload = reload
1740
self.consume_kwargs = consume_kwargs
41+
self.stop_event = threading.Event()
42+
self.sigint_timeout = sigint_timeout
43+
self.sigkill_timeout = sigkill_timeout
44+
45+
if self.reload:
46+
self.watch_filter = PythonFilter(ignore_paths=ignore_paths)
47+
self.watcher = watch(
48+
Path.cwd(),
49+
watch_filter=self.watch_filter,
50+
stop_event=self.stop_event,
51+
yield_on_timeout=True,
52+
)
53+
54+
def handle_signal(self, *args, **kwargs):
55+
self.stop_event.set()
56+
57+
def run(self):
58+
for sig in [signal.SIGINT, signal.SIGTERM]:
59+
signal.signal(sig, self.handle_signal)
60+
if self.reload:
61+
signal.signal(signal.SIGHUP, self.restart)
62+
63+
self.start()
64+
65+
if self.reload:
66+
for files_changed in self:
67+
if files_changed:
68+
self.restart()
69+
else:
70+
self.stop_event.wait()
71+
72+
self.terminate()
1873

1974
def start(self):
20-
for i in range(self.options['workers'] or 1):
21-
process = multiprocessing.Process(
22-
name=f'cqrs-consumer-{i}',
23-
target=self.transport.consume,
24-
kwargs=self.consume_kwargs,
75+
for _ in range(self.workers):
76+
process = start_process(
77+
consume,
78+
'function',
79+
(),
80+
self.consume_kwargs,
2581
)
2682
self.pool.append(process)
27-
process.start()
28-
29-
for process in self.pool:
30-
process.join()
3183

3284
def terminate(self, *args, **kwargs):
3385
while self.pool:
34-
p = self.pool.pop()
35-
p.terminate()
36-
p.join()
86+
process = self.pool.pop()
87+
process.stop(sigint_timeout=self.sigint_timeout, sigkill_timeout=self.sigkill_timeout)
3788

38-
def reload(self, *args, **kwargs):
89+
def restart(self, *args, **kwargs):
3990
self.terminate()
4091
self.start()
4192

93+
def __iter__(self):
94+
return self
95+
96+
def __next__(self):
97+
changes = next(self.watcher)
98+
if changes:
99+
return list({Path(c[1]) for c in changes})
100+
return None
101+
42102

43103
class Command(BaseCommand):
44104
help = 'Starts CQRS worker, which consumes messages from message queue.'
45105

46106
def add_arguments(self, parser):
47-
parser.add_argument('--workers', '-w', help='Number of workers', type=int, default=0)
107+
parser.add_argument(
108+
'--workers',
109+
'-w',
110+
help='Number of workers',
111+
type=int,
112+
default=1,
113+
)
48114
parser.add_argument(
49115
'--cqrs-id',
50116
'-cid',
@@ -53,35 +119,72 @@ def add_arguments(self, parser):
53119
help='Choose model(s) by CQRS_ID for consuming',
54120
)
55121
parser.add_argument(
56-
'--reload', '-r', help='Enable reload signal SIGHUP', action='store_true',
122+
'--reload',
123+
'-r',
124+
help=(
125+
'Enable reload signal SIGHUP and autoreload '
126+
'on file changes'
127+
),
128+
action='store_true',
129+
default=False,
130+
)
131+
parser.add_argument(
132+
'--ignore-paths',
133+
nargs='?',
134+
type=str,
135+
help=(
136+
'Specify directories to ignore, '
137+
'to ignore multiple paths use a comma as separator, '
138+
'e.g. "env" or "env,node_modules"'
139+
),
140+
)
141+
parser.add_argument(
142+
'--sigint-timeout',
143+
nargs='?',
144+
type=int,
145+
default=5,
146+
help='How long to wait for the sigint timeout before sending sigkill.',
147+
)
148+
parser.add_argument(
149+
'--sigkill-timeout',
150+
nargs='?',
151+
type=int,
152+
default=1,
153+
help='How long to wait for the sigkill timeout before issuing a timeout exception.',
57154
)
58155

59-
def handle(self, *args, **options):
60-
if not options['workers'] and not options['reload']:
61-
current_transport.consume(**self.get_consume_kwargs(options))
62-
return
63-
64-
self.start_workers_pool(options)
156+
def handle(
157+
self,
158+
*args,
159+
workers=1,
160+
cqrs_id=None,
161+
reload=False,
162+
ignore_paths=None,
163+
sigint_timeout=5,
164+
sigkill_timeout=1,
165+
**options,
166+
):
167+
168+
paths_to_ignore = None
169+
if ignore_paths:
170+
paths_to_ignore = [Path(p).resolve() for p in ignore_paths.split(',')]
65171

66-
def start_workers_pool(self, options):
67172
workers_manager = WorkersManager(
68-
options, current_transport, self.get_consume_kwargs(options),
173+
workers=workers,
174+
consume_kwargs=self.get_consume_kwargs(cqrs_id),
175+
reload=reload,
176+
ignore_paths=paths_to_ignore,
177+
sigint_timeout=sigint_timeout,
178+
sigkill_timeout=sigkill_timeout,
69179
)
70-
if options['reload']:
71-
try:
72-
multiprocessing.set_start_method('spawn')
73-
except RuntimeError:
74-
pass
75-
76-
signal.signal(signal.SIGHUP, workers_manager.reload)
77180

78-
workers_manager.start()
181+
workers_manager.run()
79182

80-
def get_consume_kwargs(self, options):
183+
def get_consume_kwargs(self, ids_list):
81184
consume_kwargs = {}
82-
if options.get('cqrs_id'):
185+
if ids_list:
83186
cqrs_ids = set()
84-
for cqrs_id in options['cqrs_id']:
187+
for cqrs_id in ids_list:
85188
model = ReplicaRegistry.get_model_by_cqrs_id(cqrs_id)
86189
if not model:
87190
raise CommandError('Wrong CQRS ID: {0}!'.format(cqrs_id))

integration_tests/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ services:
1212
- POSTGRES_USER=user
1313
- POSTGRES_PASSWORD=pswd
1414
- POSTGRES_DB=replica
15+
- POSTGRES_HOST_AUTH_METHOD=md5
16+
- POSTGRES_INITDB_ARGS=--auth-host=md5
1517

1618
replica:
1719
build:

integration_tests/kombu.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ services:
1616
- POSTGRES_USER=user
1717
- POSTGRES_PASSWORD=pswd
1818
- POSTGRES_DB=replica
19+
- POSTGRES_HOST_AUTH_METHOD=md5
20+
- POSTGRES_INITDB_ARGS=--auth-host=md5
1921

2022
replica:
2123
build:

integration_tests/rdbms.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ services:
99
- POSTGRES_USER=user
1010
- POSTGRES_PASSWORD=pswd
1111
- POSTGRES_DB=django_cqrs
12+
- POSTGRES_HOST_AUTH_METHOD=md5
13+
- POSTGRES_INITDB_ARGS=--auth-host=md5
1214

1315
mysql:
1416
image: mysql:8.0

requirements/dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ kombu>=4.6.*
44
ujson>=5.4.0
55
django-model-utils>=4.0.0
66
python-dateutil>=2.4
7+
watchfiles>=0.18.1

0 commit comments

Comments
 (0)