Skip to content

Conversation

@yeong-hwan
Copy link
Contributor

@yeong-hwan yeong-hwan commented Jul 3, 2025

Description

This PR adds graceful shutdown support to the Django gRPC server, inspired by Gunicorn's arbiter.py implementation. The feature addresses the PID 1 problem commonly encountered in containerized environments and ensures that the server can be safely terminated while allowing ongoing requests to complete.

Problem Statement

In containerized environments, especially Kubernetes, applications often run as PID 1 inside containers.
When PID 1 processes don't handle signals properly, they can:

  • Ignore SIGTERM signals sent by the container runtime
  • Fail to propagate signals to child processes
  • Cause containers to be forcefully killed after timeout periods
  • Lead to incomplete request processing and potential data corruption

Why gRPC servers deployed on Kubernetes may not respond to SIGTERM:

  • In Kubernetes, processes started as the container's entrypoint run as PID 1
  • On Linux, PID 1 processes do not automatically receive default signal handlers
  • In contrast, non-PID 1 processes do have default signal handlers registered automatically
  • As a result, unless explicitly handled, SIGTERM and other signals may be ignored when the server runs as PID 1

What does this PR do?

  • Signal Handling: Registers proper handlers for SIGTERM and SIGINT signals to initiate graceful shutdown
  • PID 1 Compatibility: Ensures the server can run as PID 1 in containers and handle signals correctly
  • Graceful Shutdown Process:
    • Stops accepting new connections
    • Waits for ongoing requests to complete (with grace=True)
    • Sends the grpc_shutdown signal to all connected receivers
    • Properly cleans up resources before exit
  • Async Support: Implements graceful shutdown for both synchronous and asynchronous server modes
  • Autoreload Compatibility: Maintains compatibility with Django's autoreload functionality
  • Comprehensive Testing: Includes unit tests and integration tests for the graceful shutdown functionality

Key Features

  • Signal Registration: Automatically registers SIGTERM and SIGINT handlers when the server starts
  • PID 1 Support: Properly handles signals when running as PID 1 in containerized environments
  • Resource Cleanup: Ensures proper cleanup of server resources and Django signals
  • Container-Ready: Specifically designed for containerized environments (Docker, Kubernetes)
  • Backward Compatibility: Maintains full compatibility with existing functionality

Testing

The implementation includes:

  • Unit tests for signal handler registration and functionality
  • Tests for both synchronous and asynchronous server modes
  • Integration tests that verify actual signal handling in a real process
  • Tests for autoreload compatibility
  • Tests specifically designed to verify PID 1 signal handling behavior

Comment on lines +53 to +61
def _setup_signal_handlers(self):
"""Setup signal handlers (inspired by Gunicorn arbiter.py)"""
# Store SIGTERM handler
self._original_sigterm_handler = signal.signal(signal.SIGTERM, self._handle_sigterm)

# Also set SIGINT handler (Ctrl+C)
signal.signal(signal.SIGINT, self._handle_sigterm)

self.stdout.write("Signal handlers registered for graceful shutdown")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added _setup_signal_handlers() method to register signal handlers

Comment on lines +63 to +66
def _handle_sigterm(self, signum, frame):
"""Handle SIGTERM signal to start graceful shutdown"""
self.stdout.write(f"Received signal {signum}. Starting graceful shutdown...")
self._shutdown_event.set()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added _handle_sigterm() method to handle shutdown signals

Sets the shutdown event when SIGTERM or SIGINT is received, triggering the graceful shutdown process.

Comment on lines +68 to +88
def _graceful_shutdown(self, server):
"""Gracefully shutdown the server"""
try:
# Stop accepting new connections
self.stdout.write("Stopping server from accepting new connections...")

# Stop gRPC server (with grace=True to wait for ongoing requests to complete)
if hasattr(server, 'stop'):
# For synchronous server
server.stop(grace=True)
else:
# For asynchronous server
asyncio.create_task(server.stop(grace=True))

# Send Django signal
grpc_shutdown.send(None)

self.stdout.write("Graceful shutdown completed")

except Exception as e:
self.stderr.write(f"Error during graceful shutdown: {e}")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added _graceful_shutdown() and _graceful_shutdown_async() methods for server cleanup

Handles the actual server shutdown process, stopping new connections, waiting for ongoing requests to complete, and sending Django signals.

Comment on lines 107 to +148
def _serve(self, max_workers, port, *args, **kwargs):
"""
Run gRPC server
"""
autoreload.raise_last_exception()
self.stdout.write("gRPC server starting at %s" % datetime.datetime.now())

# Only setup signal handlers when not in autoreload mode
# autoreload runs in a separate thread, not the main thread, so signal handlers cannot be registered
if not kwargs.get("autoreload", False):
self._setup_signal_handlers()

server = create_server(max_workers, port)
self._server = server

server.start()

self.stdout.write("gRPC server is listening port %s" % port)

if kwargs["list_handlers"] is True:
# Print handler list if list_handlers option is enabled (default: False)
if kwargs.get("list_handlers", False):
self.stdout.write("Registered handlers:")
for handler in extract_handlers(server):
self.stdout.write("* %s" % handler)

server.wait_for_termination()
# Send shutdown signal to all connected receivers
grpc_shutdown.send(None)
# Only execute graceful shutdown logic when not in autoreload mode
if not kwargs.get("autoreload", False):
# Wait loop for graceful shutdown
try:
while not self._shutdown_event.is_set():
time.sleep(0.1)
except KeyboardInterrupt:
self.stdout.write("Received keyboard interrupt, starting graceful shutdown...")
self._shutdown_event.set()

# Perform graceful shutdown
self._graceful_shutdown(server)
else:
# Use original wait_for_termination for autoreload mode
server.wait_for_termination()
# Send shutdown signal to all connected receivers
grpc_shutdown.send(None)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified _serve() and _serve_async() methods to include graceful shutdown logic

Integrates signal handling and graceful shutdown logic into the main server serving methods

@yeong-hwan yeong-hwan marked this pull request as ready for review July 3, 2025 08:59
@gluk-w gluk-w merged commit 2f5eb24 into gluk-w:master Sep 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants