11import argparse
22import os
33import 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
512from flowgrid import FlowGrid
613from 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+
959def 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+
45216def 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
79237if __name__ == '__main__' :
0 commit comments