Skip to content

Commit 47abbc4

Browse files
authored
Merge pull request #106 from cloudblue/support_autoreload_for_development
LITE-25360 support runner autoreload
2 parents 5bf8d17 + e690244 commit 47abbc4

File tree

13 files changed

+444
-142
lines changed

13 files changed

+444
-142
lines changed

connect/eaas/runner/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
'WebApp': 'Web Application',
135135
}
136136

137-
PROCESS_CHECK_INTERVAL_SECS = 5
137+
PROCESS_CHECK_INTERVAL_SECS = 1
138138

139139
LEVEL_TO_FONT_COLOR = {
140140
'INFO': ('ansi_regular', 'BLUE'),

connect/eaas/runner/main.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ def start(data):
3434
if not data.no_validate:
3535
highest_message_level, tables = validate_extension()
3636

37-
master = Master(not data.unsecure)
37+
master = Master(
38+
secure=not data.unsecure,
39+
debug=data.debug,
40+
no_rich=data.no_rich_logging,
41+
reload=data.reload,
42+
)
3843

3944
have_features, features = master.get_available_features()
4045

@@ -68,11 +73,36 @@ def start(data):
6873

6974
def main():
7075
parser = argparse.ArgumentParser(prog='cextrun')
71-
parser.add_argument('-u', '--unsecure', action='store_true')
72-
parser.add_argument('-s', '--split', action='store_true', default=False)
73-
parser.add_argument('-d', '--debug', action='store_true', default=False)
74-
parser.add_argument('-n', '--no-validate', action='store_true', default=False)
75-
parser.add_argument('--no-rich-logging', action='store_true', default=False)
76+
parser.add_argument('-u', '--unsecure', action='store_true', help=argparse.SUPPRESS)
77+
parser.add_argument('-s', '--split', action='store_true', default=False, help=argparse.SUPPRESS)
78+
parser.add_argument(
79+
'-d',
80+
'--debug',
81+
action='store_true',
82+
default=False,
83+
help='Set the log level to DEBUG.',
84+
)
85+
parser.add_argument(
86+
'-n',
87+
'--no-validate',
88+
action='store_true',
89+
default=False,
90+
help='Skip extension validations and start the runner.',
91+
)
92+
parser.add_argument(
93+
'-r',
94+
'--reload',
95+
action='store_true',
96+
default=False,
97+
help='Reload the runner when a python file changes (use only for development).',
98+
)
99+
100+
parser.add_argument(
101+
'--no-rich-logging',
102+
action='store_true',
103+
default=False,
104+
help='Turn the rich console log to off.',
105+
)
76106
data = parser.parse_args()
77107
configure_logger(data.debug, data.no_rich_logging)
78108
if not data.no_validate:

connect/eaas/runner/master.py

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import threading
33
import signal
44
import time
5-
from multiprocessing import Process
5+
from pathlib import Path
6+
7+
from watchfiles import watch
8+
from watchfiles.filters import PythonFilter
9+
from watchfiles.run import start_process
610

711
from connect.eaas.runner.config import ConfigHelper
812
from connect.eaas.runner.constants import (
@@ -28,6 +32,16 @@
2832
logger = logging.getLogger('connect.eaas')
2933

3034

35+
HANDLED_SIGNALS = (signal.SIGINT, signal.SIGTERM)
36+
37+
38+
def _display_path(path):
39+
try:
40+
return f'"{path.relative_to(Path.cwd())}"'
41+
except ValueError: # pragma: no cover
42+
return f'"{path}"'
43+
44+
3145
class Master:
3246

3347
HANDLER_CLASSES = {
@@ -44,15 +58,31 @@ class Master:
4458
ANVILAPP_WORKER: start_anvilapp_worker_process,
4559
}
4660

47-
def __init__(self, secure=True):
61+
def __init__(self, secure=True, debug=False, no_rich=False, reload=False):
4862
self.config = ConfigHelper(secure=secure)
4963
self.handlers = {
5064
worker_type: self.HANDLER_CLASSES[worker_type](self.config)
5165
for worker_type in WORKER_TYPES
5266
}
67+
self.reload = reload
68+
self.debug = debug
69+
self.no_rich = no_rich
5370
self.workers = {}
54-
self.exited_workers = {}
5571
self.stop_event = threading.Event()
72+
self.monitor_event = threading.Event()
73+
self.watch_filter = PythonFilter(ignore_paths=None)
74+
self.watcher = watch(
75+
Path.cwd(),
76+
watch_filter=self.watch_filter,
77+
stop_event=self.stop_event,
78+
yield_on_timeout=True,
79+
)
80+
self.monitor_thread = None
81+
self.setup_signals_handler()
82+
83+
def setup_signals_handler(self):
84+
for sig in HANDLED_SIGNALS:
85+
signal.signal(sig, self.handle_signal)
5686

5787
def get_available_features(self):
5888
have_features = False
@@ -67,38 +97,70 @@ def get_available_features(self):
6797
features[handler.__class__.__name__] = worker_info
6898
return have_features, features
6999

70-
def start_worker_process(self, worker_type, handler):
71-
p = Process(
72-
target=self.PROCESS_TARGETS[worker_type],
73-
args=(handler,),
74-
)
75-
self.workers[worker_type] = p
76-
p.start()
77-
logger.info(f'{worker_type.capitalize()} worker pid: {p.pid}')
100+
def handle_signal(self, *args, **kwargs):
101+
self.stop_event.set()
78102

79-
def run(self):
103+
def start(self):
80104
for worker_type, handler in self.handlers.items():
81105
if handler.should_start:
82-
self.exited_workers[worker_type] = False
83106
self.start_worker_process(worker_type, handler)
107+
self.monitor_thread = threading.Thread(target=self.monitor_processes)
108+
self.monitor_event.set()
109+
self.monitor_thread.start()
84110

85-
def _terminate(*args, **kwargs): # pragma: no cover
86-
for p in self.workers.values():
87-
p.join()
88-
self.stop_event.set()
89-
90-
signal.signal(signal.SIGINT, _terminate)
91-
signal.signal(signal.SIGTERM, _terminate)
111+
def start_worker_process(self, worker_type, handler):
112+
p = start_process(
113+
self.PROCESS_TARGETS[worker_type],
114+
'function',
115+
(handler, self.debug, self.no_rich),
116+
{},
117+
)
118+
self.workers[worker_type] = p
119+
logger.info(f'{worker_type.capitalize()} worker pid: {p.pid}')
92120

93-
while not (self.stop_event.is_set() or all(self.exited_workers.values())):
121+
def monitor_processes(self):
122+
while self.monitor_event.is_set():
94123
for worker_type, p in self.workers.items():
95124
if not p.is_alive():
96125
if p.exitcode != 0:
97126
notify_process_restarted(worker_type)
98127
logger.info(f'Process of type {worker_type} is dead, restart it')
99128
self.start_worker_process(worker_type, self.handlers[worker_type])
100129
else:
101-
self.exited_workers[worker_type] = True
102130
logger.info(f'{worker_type.capitalize()} worker exited')
103131

104132
time.sleep(PROCESS_CHECK_INTERVAL_SECS)
133+
134+
def stop(self):
135+
self.monitor_event.clear()
136+
self.monitor_thread.join()
137+
for process in self.workers.values():
138+
process.stop(sigint_timeout=5, sigkill_timeout=1)
139+
logger.info(f'Consumer process with pid {process.pid} stopped.')
140+
141+
def restart(self):
142+
self.stop()
143+
self.start()
144+
145+
def __iter__(self):
146+
return self
147+
148+
def __next__(self):
149+
changes = next(self.watcher)
150+
if changes:
151+
return list({Path(c[1]) for c in changes})
152+
return None
153+
154+
def run(self):
155+
self.start()
156+
if self.reload:
157+
for files_changed in self:
158+
if files_changed:
159+
logger.warning(
160+
'Detected changes in %s. Reloading...',
161+
', '.join(map(_display_path, files_changed)),
162+
)
163+
self.restart()
164+
else:
165+
self.stop_event.wait()
166+
self.stop()

connect/eaas/runner/workers/anvil.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
SetupRequest,
1616
)
1717
from connect.eaas.runner.workers.base import WorkerBase
18-
from connect.eaas.runner.helpers import get_version
18+
from connect.eaas.runner.helpers import configure_logger, get_version
1919

2020

2121
logger = logging.getLogger(__name__)
@@ -66,7 +66,8 @@ async def shutdown(self):
6666
await super().shutdown()
6767

6868

69-
def start_anvilapp_worker_process(handler):
69+
def start_anvilapp_worker_process(handler, debug, no_rich):
70+
configure_logger(debug, no_rich)
7071
worker = AnvilWorker(handler)
7172
loop = asyncio.get_event_loop()
7273
loop.add_signal_handler(

connect/eaas/runner/workers/events.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
RESULT_SENDER_MAX_RETRIES,
2121
RESULT_SENDER_WAIT_GRACE_SECONDS,
2222
)
23-
from connect.eaas.runner.helpers import get_version
23+
from connect.eaas.runner.helpers import configure_logger, get_version
2424
from connect.eaas.runner.managers import (
2525
BackgroundTasksManager,
2626
InteractiveTasksManager,
@@ -201,7 +201,8 @@ def __repr__(self):
201201
return super().__repr__()
202202

203203

204-
def _start_event_worker_process(handler, runner_type):
204+
def _start_event_worker_process(handler, runner_type, debug, no_rich):
205+
configure_logger(debug, no_rich)
205206
worker = EventsWorker(handler, runner_type=runner_type)
206207
loop = asyncio.get_event_loop()
207208
loop.add_signal_handler(
@@ -215,9 +216,9 @@ def _start_event_worker_process(handler, runner_type):
215216
loop.run_until_complete(worker.start())
216217

217218

218-
def start_interactive_worker_process(handler):
219-
_start_event_worker_process(handler, 'interactive')
219+
def start_interactive_worker_process(handler, debug, no_rich):
220+
_start_event_worker_process(handler, 'interactive', debug, no_rich)
220221

221222

222-
def start_background_worker_process(handler):
223-
_start_event_worker_process(handler, 'background')
223+
def start_background_worker_process(handler, debug, no_rich):
224+
_start_event_worker_process(handler, 'background', debug, no_rich)

connect/eaas/runner/workers/web.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
WebTask,
2323
)
2424
from connect.eaas.runner.workers.base import WorkerBase
25-
from connect.eaas.runner.helpers import get_version
25+
from connect.eaas.runner.helpers import configure_logger, get_version
2626

2727

2828
logger = logging.getLogger(__name__)
@@ -174,7 +174,8 @@ def build_response(self, task, status, headers, body):
174174
return message.serialize()
175175

176176

177-
def start_webapp_worker_process(handler):
177+
def start_webapp_worker_process(handler, debug, no_rich):
178+
configure_logger(debug, no_rich)
178179
worker = WebWorker(handler)
179180
loop = asyncio.get_event_loop()
180181
loop.add_signal_handler(

0 commit comments

Comments
 (0)