From 8f8f88540e24b13df6ee878201f3295df77d574b Mon Sep 17 00:00:00 2001 From: "Adam J. Weigold" Date: Wed, 12 Nov 2025 10:43:29 -0600 Subject: [PATCH] Reduced zombie processes when running in a container by utilizing a python requests based health check that can reap stalled checks. Fixed the health endpoint. Added SIGCHLD handler to main process to reap terminated child processes. --- Dockerfile | 5 +++-- main.py | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77dc10c6..f1dc9e46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,9 +30,10 @@ ENV TZ=UTC # Expose port EXPOSE 9705 -# Add health check for Docker +# Add health check for Docker using Python to avoid spawning curl processes +# The SIGCHLD handler in main.py will reap any terminated health check processes HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:9705/health || exit 1 + CMD ["python3", "-c", "import requests; import sys; r = requests.get('http://localhost:9705/api/health', timeout=5); sys.exit(0 if r.status_code == 200 else 1)"] # Run the main application using the new entry point CMD ["python3", "main.py"] \ No newline at end of file diff --git a/main.py b/main.py index c6d486ea..793bfdae 100644 --- a/main.py +++ b/main.py @@ -324,11 +324,28 @@ def run_web_server(): if not stop_event.is_set(): stop_event.set() +def sigchld_handler(signum, frame): + """Handle SIGCHLD to prevent zombie processes.""" + # Reap all terminated child processes without blocking + while True: + try: + # WNOHANG returns immediately if no child has exited + pid, status = os.waitpid(-1, os.WNOHANG) + if pid == 0: + # No more zombie children + break + except ChildProcessError: + # No child processes + break + except OSError: + # Error occurred, stop trying + break + def main_shutdown_handler(signum, frame): """Gracefully shut down the application.""" global _global_shutdown_flag _global_shutdown_flag = True # Set global shutdown flag immediately - + signal_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM" if signum == signal.SIGTERM else f"Signal {signum}" huntarr_logger.info(f"Received {signal_name}. Initiating graceful shutdown...") @@ -440,7 +457,10 @@ def main(): # Register signal handlers for graceful shutdown in the main process signal.signal(signal.SIGINT, main_shutdown_handler) signal.signal(signal.SIGTERM, main_shutdown_handler) - + + # Register SIGCHLD handler to prevent zombie processes (Docker healthchecks) + signal.signal(signal.SIGCHLD, sigchld_handler) + # Register cleanup handler atexit.register(cleanup_handler)