-
-
Notifications
You must be signed in to change notification settings - Fork 333
feat: add --max-requests for worker process recycling #1354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
53ddacc
62ce87c
bba766c
e2ddef7
7546da5
cfe0649
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
|
|
||
| from robyn.events import Events | ||
| from robyn.logger import logger | ||
| from robyn.robyn import FunctionInfo, Headers, Server, SocketHeld | ||
| from robyn.robyn import FunctionInfo, Headers, Server, SocketHeld, get_request_count | ||
| from robyn.router import GlobalMiddleware, Route, RouteMiddleware | ||
| from robyn.types import Directory | ||
|
|
||
|
|
@@ -30,7 +30,10 @@ def run_processes( | |
| open_browser: bool, | ||
| client_timeout: int = 30, | ||
| keep_alive_timeout: int = 20, | ||
| max_requests: Optional[int] = None, | ||
| ) -> List[Process]: | ||
| import time | ||
|
|
||
| socket = SocketHeld(url, port) | ||
|
|
||
| process_pool = init_processpool( | ||
|
|
@@ -48,12 +51,24 @@ def run_processes( | |
| excluded_response_headers_paths, | ||
| client_timeout, | ||
| keep_alive_timeout, | ||
| max_requests, | ||
| ) | ||
|
|
||
| shutting_down = False | ||
|
|
||
| def terminating_signal_handler(_sig, _frame): | ||
| logger.info("Terminating server!!", bold=True) | ||
| nonlocal shutting_down | ||
| shutting_down = True | ||
| logger.info("Gracefully shutting down server...", bold=True) | ||
| for process in process_pool: | ||
| process.terminate() | ||
| for process in process_pool: | ||
| process.kill() | ||
| process.join(timeout=30) | ||
| if process.is_alive(): | ||
| logger.warning("Process %s did not shut down in time, forcing kill.", process.pid) | ||
| process.kill() | ||
| process.join(timeout=5) | ||
| sys.exit(0) | ||
|
|
||
| signal.signal(signal.SIGINT, terminating_signal_handler) | ||
| signal.signal(signal.SIGTERM, terminating_signal_handler) | ||
|
|
@@ -63,8 +78,38 @@ def terminating_signal_handler(_sig, _frame): | |
| webbrowser.open_new_tab(f"http://{url}:{port}/") | ||
|
|
||
| logger.info("Press Ctrl + C to stop \n") | ||
| for process in process_pool: | ||
| process.join() | ||
|
|
||
| if max_requests and max_requests > 0 and len(process_pool) > 0: | ||
| while not shutting_down: | ||
| for i, process in enumerate(process_pool): | ||
| if not process.is_alive() and not shutting_down: | ||
| logger.info("Worker process exited (recycling), spawning replacement.") | ||
| copied_socket = socket.try_clone() | ||
| new_process = Process( | ||
| target=spawn_process, | ||
| args=( | ||
| directories, | ||
| request_headers, | ||
| routes, | ||
| global_middlewares, | ||
| route_middlewares, | ||
| web_sockets, | ||
| event_handlers, | ||
| copied_socket, | ||
| workers, | ||
| response_headers, | ||
| excluded_response_headers_paths, | ||
| client_timeout, | ||
| keep_alive_timeout, | ||
| max_requests, | ||
| ), | ||
| ) | ||
| new_process.start() | ||
| process_pool[i] = new_process | ||
| time.sleep(5) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+82
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize Line 82 and Line 136 use truthiness, but 💡 Minimal guardrail def run_processes(
@@
keep_alive_timeout: int = 20,
max_requests: Optional[int] = None,
) -> List[Process]:
+ if max_requests is not None and max_requests <= 0:
+ max_requests = None
@@
- if max_requests and max_requests > 0 and len(process_pool) > 0:
+ if max_requests is not None and len(process_pool) > 0:
while not shutting_down:
... def init_processpool(
@@
keep_alive_timeout: int = 20,
max_requests: Optional[int] = None,
) -> List[Process]:
+ if max_requests is not None and max_requests <= 0:
+ max_requests = None
+ if sys.platform.startswith("win32") and max_requests is not None:
+ raise RuntimeError("--max-requests is not supported on Windows yet")
@@
- if sys.platform.startswith("win32") or (processes == 1 and not max_requests):
+ if sys.platform.startswith("win32") or (processes == 1 and max_requests is None):
spawn_process(...)Also applies to: 133-152 🤖 Prompt for AI AgentsWorker replacement starts late enough to cause outages. The parent only notices exited workers on the next 5-second sleep boundary at Line 110. In 🤖 Prompt for AI Agents |
||
| else: | ||
| for process in process_pool: | ||
| process.join() | ||
|
|
||
| return process_pool | ||
|
|
||
|
|
@@ -84,6 +129,7 @@ def init_processpool( | |
| excluded_response_headers_paths: Optional[List[str]], | ||
| client_timeout: int = 30, | ||
| keep_alive_timeout: int = 20, | ||
| max_requests: Optional[int] = None, | ||
| ) -> List[Process]: | ||
| process_pool: List = [] | ||
| if sys.platform.startswith("win32") or processes == 1: | ||
|
|
@@ -101,6 +147,7 @@ def init_processpool( | |
| excluded_response_headers_paths, | ||
| client_timeout, | ||
| keep_alive_timeout, | ||
| max_requests, | ||
| ) | ||
|
|
||
| return process_pool | ||
|
|
@@ -123,6 +170,7 @@ def init_processpool( | |
| excluded_response_headers_paths, | ||
| client_timeout, | ||
| keep_alive_timeout, | ||
| max_requests, | ||
| ), | ||
| ) | ||
| process.start() | ||
|
|
@@ -161,6 +209,7 @@ def spawn_process( | |
| excluded_response_headers_paths: Optional[List[str]], | ||
| client_timeout: int = 30, | ||
| keep_alive_timeout: int = 20, | ||
| max_requests: Optional[int] = None, | ||
| ): | ||
| """ | ||
| This function is called by the main process handler to create a server runtime. | ||
|
|
@@ -175,14 +224,13 @@ def spawn_process( | |
| :param socket SocketHeld: This is the main tcp socket, which is being shared across multiple processes. | ||
| :param process_name string: This is the name given to the process to identify the process | ||
| :param workers int: This is the name given to the process to identify the process | ||
| :param max_requests Optional[int]: Recycle this worker after N requests | ||
| """ | ||
|
|
||
| loop = initialize_event_loop() | ||
|
|
||
| server = Server() | ||
|
|
||
| # TODO: if we remove the dot access | ||
| # the startup time will improve in the server | ||
| for directory in directories: | ||
| server.add_directory(*directory.as_list()) | ||
|
|
||
|
|
@@ -220,6 +268,20 @@ def spawn_process( | |
| try: | ||
| server.start(socket, workers) | ||
| loop = asyncio.get_event_loop() | ||
|
|
||
| if max_requests and max_requests > 0: | ||
|
|
||
| def _check_max_requests(): | ||
| if get_request_count() >= max_requests: | ||
| logger.info("Max requests (%d) reached, worker shutting down for recycling.", max_requests) | ||
| loop.stop() | ||
| else: | ||
| loop.call_later(5, _check_max_requests) | ||
|
|
||
| loop.call_later(5, _check_max_requests) | ||
|
|
||
| loop.run_forever() | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| except KeyboardInterrupt: | ||
| pass | ||
| finally: | ||
| loop.close() | ||
Uh oh!
There was an error while loading. Please reload this page.