Skip to content

Commit dade549

Browse files
committed
✨ Added optional reload parameter (detect file changes)
1 parent 27ff12d commit dade549

2 files changed

Lines changed: 190 additions & 31 deletions

File tree

flowgrid/cli.py

Lines changed: 188 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,68 @@
11
import argparse
22
import os
33
import sys
4+
import time
5+
import multiprocessing
6+
7+
from typing import Optional, List
8+
9+
from watchdog.observers import Observer
10+
from watchdog.events import FileSystemEventHandler
411

512
from flowgrid import FlowGrid
613
from flowgrid.lazy_import import lazy
714

815

16+
def start_celery_worker(
17+
app: str,
18+
concurrency: int,
19+
loglevel: str,
20+
) -> None:
21+
'''
22+
Start a Celery worker process.
23+
24+
Args:
25+
app: FlowGrid app name
26+
concurrency: Number of concurrent workers
27+
loglevel: Logging level
28+
'''
29+
# This is to ensure that lazy import works
30+
cwd = os.getcwd()
31+
sys.path.append(cwd)
32+
33+
try:
34+
data = app.rsplit('.', 1)
35+
if len(data) != 2:
36+
raise ValueError('Invalid app name')
37+
app_module = data[0]
38+
app_name = data[1]
39+
module = lazy(app_module)
40+
fg = getattr(module, app_name)
41+
except Exception as e:
42+
print(f'Error loading app: {app}')
43+
print(e)
44+
sys.exit(1)
45+
46+
if not isinstance(fg, FlowGrid):
47+
print(
48+
f'Invalid app. {app} is not a FlowGrid instance it is a {type(fg)}'
49+
)
50+
sys.exit(1)
51+
52+
fg.celery_app.worker_main([
53+
'worker',
54+
f'--concurrency={concurrency}',
55+
f'--loglevel={loglevel}'
56+
])
57+
58+
959
def main():
1060
parser = argparse.ArgumentParser(description='FlowGrid')
1161
subparsers = parser.add_subparsers(dest='command')
1262

1363
worker_parser = subparsers.add_parser(
1464
'worker',
15-
help="Launch a FlowGrid worker"
65+
help='Launch a FlowGrid worker'
1666
)
1767

1868
worker_parser.add_argument(
@@ -31,49 +81,157 @@ def main():
3181
'--loglevel', '-l',
3282
type=str,
3383
default='info',
34-
help='Loggign level (info, debug, warning, error).'
84+
help='Logging level (info, debug, warning, error).'
85+
)
86+
worker_parser.add_argument(
87+
'--reload', '-r',
88+
action='store_true',
89+
help='Enable auto-reload when Python files change'
90+
)
91+
worker_parser.add_argument(
92+
'--watch-dir', '-w',
93+
type=str,
94+
action='append',
95+
help=(
96+
'Additional directories to watch for changes '
97+
'(can be specified multiple times)'
98+
)
3599
)
36100

37101
args = parser.parse_args()
38102
if args.command == 'worker':
39-
start_worker(args.app, args.concurrency, args.loglevel)
103+
start_worker(
104+
args.app,
105+
args.concurrency,
106+
args.loglevel,
107+
args.reload,
108+
args.watch_dir,
109+
)
40110
else:
41111
parser.print_help()
42112
sys.exit(1)
43113

44114

115+
class ReloadableWorker:
116+
'''
117+
A worker that can be reloaded when Python files change.
118+
119+
Args:
120+
app: FlowGrid app name
121+
concurrency: Number of concurrent workers
122+
loglevel: Logging level (info, debug, warning, error)
123+
reload: Enable auto-reload when Python files change
124+
watch_dirs: Additional directories to watch for changes
125+
'''
126+
127+
def __init__(
128+
self,
129+
app: str,
130+
concurrency: int,
131+
loglevel: str,
132+
reload: bool = False,
133+
watch_dirs: Optional[List[str]] = None,
134+
):
135+
self.app = app
136+
self.concurrency = concurrency
137+
self.loglevel = loglevel
138+
self.reload = reload
139+
self.watch_dirs = watch_dirs or [os.getcwd()]
140+
self.worker_process = None
141+
self.stop_event = multiprocessing.Event()
142+
143+
def _start_worker(self):
144+
'''Start a new worker process.'''
145+
# Terminate existing process if it exists
146+
if self.worker_process and self.worker_process.is_alive():
147+
self.worker_process.terminate()
148+
self.worker_process.join(timeout=5)
149+
if self.worker_process.is_alive():
150+
self.worker_process.kill()
151+
152+
# Create and start a new worker process
153+
self.worker_process = multiprocessing.Process(
154+
target=start_celery_worker,
155+
args=(self.app, self.concurrency, self.loglevel)
156+
)
157+
self.worker_process.start()
158+
159+
def _create_file_handler(self):
160+
'''Create a file system event handler for detecting changes.'''
161+
class ReloadHandler(FileSystemEventHandler):
162+
def __init__(self, stop_event):
163+
self.stop_event = stop_event
164+
165+
def on_modified(self, event):
166+
if not event.is_directory and event.src_path.endswith('.py'):
167+
print(
168+
f'Detected change in {event.src_path}. '
169+
'Restarting worker...'
170+
)
171+
self.stop_event.set()
172+
173+
return ReloadHandler(self.stop_event)
174+
175+
def run(self):
176+
'''Run the worker, with optional file watching and reloading.'''
177+
if not self.reload:
178+
# If reload is not enabled, just run the worker once
179+
# In current process
180+
start_celery_worker(self.app, self.concurrency, self.loglevel)
181+
return
182+
183+
# Start initial worker
184+
self._start_worker()
185+
186+
# Set up file watching
187+
event_handler = self._create_file_handler()
188+
observer = Observer()
189+
for watch_dir in self.watch_dirs:
190+
observer.schedule(event_handler, watch_dir, recursive=True)
191+
observer.start()
192+
193+
try:
194+
while True:
195+
# Wait for file changes
196+
while not self.stop_event.is_set():
197+
time.sleep(0.5)
198+
# Check if worker process is still alive
199+
if not self.worker_process.is_alive():
200+
break
201+
202+
# Restart worker on file change
203+
self._start_worker()
204+
self.stop_event.clear()
205+
206+
except KeyboardInterrupt:
207+
print('Stopping worker...')
208+
finally:
209+
# Clean up
210+
if self.worker_process and self.worker_process.is_alive():
211+
self.worker_process.terminate()
212+
observer.stop()
213+
observer.join()
214+
215+
45216
def start_worker(
46217
app: str,
47218
concurrency: int,
48219
loglevel: str,
49-
):
50-
# This is to ensure that lazy import works
51-
cwd = os.getcwd()
52-
sys.path.append(cwd)
220+
reload: bool = False,
221+
watch_dirs: Optional[List[str]] = None
222+
) -> None:
223+
'''
224+
Start a FlowGrid worker.
53225
54-
try:
55-
data = app.rsplit('.', 1)
56-
if len(data) != 2:
57-
raise ValueError('Invalid app name')
58-
app = data[0]
59-
module = lazy(app)
60-
fg = getattr(module, data[1])
61-
except Exception as e:
62-
print(f'Error loading app: {app}')
63-
print(e)
64-
sys.exit(1)
65-
66-
if not isinstance(fg, FlowGrid):
67-
print(
68-
f'Invalid app. {app} is not a FlowGrid instance it is a {type(fg)}'
69-
)
70-
sys.exit(1)
71-
72-
fg.celery_app.worker_main([
73-
'worker',
74-
f'--concurrency={concurrency}',
75-
f'--loglevel={loglevel}'
76-
])
226+
Args:
227+
app: FlowGrid app name
228+
concurrency: Number of concurrent workers
229+
loglevel: Logging level (info, debug, warning, error)
230+
reload: Enable auto-reload when Python files change
231+
watch_dirs: Additional directories to watch for changes
232+
'''
233+
worker = ReloadableWorker(app, concurrency, loglevel, reload, watch_dirs)
234+
worker.run()
77235

78236

79237
if __name__ == '__main__':

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
celery==5.4.0
2-
redis==5.0.8
2+
redis==5.0.8
3+
watchdog==4.0.0

0 commit comments

Comments
 (0)